Transaction

AB182B7F690494…451060CA90FA

Block 791,555 · index 0 · indexed

Summary

Hash
AB182B7F690494AA818DEE944A1E8A09B6339C69107BE891F7CB451060CA90FA
Block
791,555
Size
91475 bytes
Gas used
118,634,163 / 178,951,300
Fee
178952 ugnot
Status
success

Messages

Attached funds
100000000 ugnot

Arguments · 13

  1. #1hlmailbox
  2. #2flags.gno
  3. #3package hlmailbox import ( "chain" "encoding/hex" "strconv" "strings" "time" "gno.land/p/nt/avl/v0" "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hlroute/v0" ) // The guardians and the watch keys (bridge/PLAN.md §5.1, §5.3): six numbered seats — 1 to 4 the guardians (3 and 4 // the outsiders'), 5 and 6 the operator's two monitors' watch keys — each holding one gno address, and three pause // flags per seat per route: sends (refuses the route's TransferRemote), credits (refuses its Process) and payouts // (refuses its Claim). A function of a route is refused while any seat's flag for it stands. A seat sets and clears // its own flags; GovDAO's unpause clears every seat's flag for the functions it names, and a seat removed or // replaced loses its flags. A guardian's payouts flag may carry the message ids it suspects — a list, or a range of // credits (hlroute.Range: "heights:<from>-<to>", or "since:<from>" for the verifier's credits from that height on, // so one flag covers a flood larger than a list) — which is what unlocks GovDAO's at-once void of those claims; a // watch key's never does. // // The meters (§5.4) start empty after every unpause, or removal, that clears a STANDING payouts flag: this realm // counts those events per route (the payouts epoch) and the route realms read the count. const ( seatCount = 6 guardians = 4 FnSends = "sends" FnCredits = "credits" FnPayouts = "payouts" maxFlagIDs = 256 // ids a payouts flag may name // The launch seats (§5.3, §11.9): PLACEHOLDERS until the four people and the two monitors have keys — seat 1 // is Jae's cold key, the rest derived test addresses — replaced in the deployed source the launch vote reads. launchSeat1 = address("g1562mcsvwrd2hm3nj7and8ggexdqd3hnag20cmv") launchSeat2 = address("g16l5nrlrp9pd8rdld7vzylrthfusv30apeq0n6z") launchSeat3 = address("g1acuz6nhl9hyka2a4t9t965kmd8t355hy7ukur5") launchSeat4 = address("g1d6nckzj9n8udw8apwrxfg929p8zm330d664ucy") launchSeat5 = address("g1m8d47hlnmp77rpkamxphlx9q6jfvejt89j7hfj") launchSeat6 = address("g1gdudkuvqdxwfncvmualymvyufdj8u7m893czuv") ) var ( seats [seatCount + 1]address // by seat number; 0 unused routes avl.Tree // pkgpath → *routeRec flags avl.Tree // flagKey(route, fn, seat) → *flag 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) 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 ) // The routes a successor announces for (AQ5): a migrate names only a successor announced for its own route, so a // slip never sends the GNOT lock to a realm whose code moves BUBBLE, or the other way round. const ( SymbolGNOT = "GNOT" SymbolBUBBLE = "BUBBLE" ) // routeRec is one route realm's record here: its payouts epoch, its place in the chain of migrations (AQ1), the // kind 6 that enrolled it on Solana (AR1), whether it pulled page 0 of its predecessor's migration, and the // successors its own re-points dropped (R35). type routeRec struct { epoch int64 // bumps whenever an unpause or a removal clears a standing payouts flag epochAt int64 // unix time of the last bump predecessor string // the route whose migrate registered this one; "" for a launch route successor string // the route this one's migrate registered; "" until it migrated 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 enrolledAt int64 // unix time of that dispatch 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 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 } // maxChain bounds the walk along a route's predecessors (Process): a chain never cycles, since AddRoute refuses a // path that is a route already, and a bridge sees a handful of migrations in its life. const maxChain = 16 // flag is one seat's standing flag on one function of one route. type flag struct { seat int at int64 // unix time it was set, or last re-asserted ids []string // a guardian's payouts flag: the message ids it suspects (none for the other flags) … ranged bool // … or the range of credits it suspects rng hlroute.Range // } func initSeats() { seats[1], seats[2], seats[3], seats[4], seats[5], seats[6] = launchSeat1, launchSeat2, launchSeat3, launchSeat4, launchSeat5, launchSeat6 } func flagKey(route, fn string, seat int) string { return route + "|" + fn + "|" + strconv.Itoa(seat) } func flagPrefix(route, fn string) string { return route + "|" + fn + "|" } func isFn(fn string) bool { return fn == FnSends || fn == FnCredits || fn == FnPayouts } func now() int64 { return time.Now().Unix() } // ------------------------------------------------------------- reading -- // Seat is the address in a seat (1–4 guardians, 5–6 watch keys); "" if the seat is empty. func Seat(n int) address { if n < 1 || n > seatCount { return "" } return seats[n] } // SeatOf is the seat an address holds, 0 if none. func SeatOf(a address) int { if a == "" { return 0 } for i := 1; i <= seatCount; i++ { if seats[i] == a { return i } } return 0 } // IsGuardianSeat is whether a seat number is a guardian's (1–4) rather than a watch key's (5–6). func IsGuardianSeat(n int) bool { return n >= 1 && n <= guardians } // IsRoute is whether a pkgpath is one of the route realms this mailbox keeps flags for. func IsRoute(path string) bool { return routes.Has(path) } // Routes lists the route realms' pkgpaths. func Routes() []string { var out []string routes.Iterate("", "", func(k string, _ any) bool { out = append(out, k) return false }) return out } // DroppedRoutes lists the dropped successors' pkgpaths (R35's tombstones; Dropped names each one's predecessor): // routes no more, whose addresses the routes still refuse as a send's recipient and sweep at a credit (AT6), as a // registered route's — a holder may have pasted one as a deposit address while its page printed it as the lock's // holder to be, and a claim credited to it would be paid into a realm no counter of the line holds. Bounded by the // re-points a bridge sees in its life, as Routes is by its migrations. func DroppedRoutes() []string { var out []string droppedRoutes.Iterate("", "", func(k string, _ any) bool { out = append(out, k) return false }) return out } // Paused is what the routes ask: whether any seat's flag for fn stands on route. func Paused(route, fn string) bool { found := false flags.Iterate(flagPrefix(route, fn), flagPrefix(route, fn)+"~", func(_ string, _ any) bool { found = true return true }) return found } // Flagged is whether some guardian's standing payouts flag on route names this claim — by its message id, or by a // range covering the height it was credited at (a "since" range only if the verifier credited it): the test that // makes GovDAO's void of that claim at once (§5.3). A watch key's flag never counts. func Flagged(route, id string, height int64, verifier bool) bool { id = cleanID(id) found := false flags.Iterate(flagPrefix(route, FnPayouts), flagPrefix(route, FnPayouts)+"~", func(_ string, v any) bool { f := v.(*flag) if !IsGuardianSeat(f.seat) { return false } if f.ranged && f.rng.Covers(height, verifier) { found = true return true } for _, x := range f.ids { if x == id { found = true return true } } return false }) return found } // PayoutsEpoch is how many times an unpause or a removal cleared a standing payouts flag on route, and when the // last one did: the routes empty their meters when it changes (§5.4). func PayoutsEpoch(route string) (epoch, at int64) { v := routes.Get(route) if v == nil { return 0, 0 } r := v.(*routeRec) return r.epoch, r.epochAt } // Flags lists the standing flags of a route as "fn seat@time[ids…]" lines. func Flags(route string) []string { var out []string flags.Iterate(route+"|", route+"|~", func(k string, v any) bool { f := v.(*flag) fn := strings.Split(k, "|")[1] s := fn + " by seat " + strconv.Itoa(f.seat) + " (" + string(seats[f.seat]) + ") at " + strconv.FormatInt(f.at, 10) if f.ranged { s += " naming " + f.rng.String() } else if len(f.ids) > 0 { s += " naming " + strings.Join(f.ids, ",") } out = append(out, s) return false }) return out } // ------------------------------------------------------------- flagging -- func mustBeRoute(cur realm) string { if !cur.IsCurrent() { panic("hlmailbox: that realm value is not live; pass your own cur") } path := cur.Previous().PkgPath() if !routes.Has(path) { panic("hlmailbox: only a route realm, not " + path) } return path } // parseIDs reads what a payouts flag names: a range (hlroute.ParseRange's forms), or a comma-separated list of // message ids, at most maxFlagIDs; "" names nothing. func parseIDs(s string) (ids []string, rng hlroute.Range, ranged bool) { if strings.TrimSpace(s) == "" { return nil, rng, false } if r, ok := hlroute.ParseRange(s); ok { return nil, r, true } parts := strings.Split(s, ",") if len(parts) > maxFlagIDs { panic("hlmailbox: more than " + strconv.Itoa(maxFlagIDs) + " ids") } out := make([]string, 0, len(parts)) for _, p := range parts { id := cleanID(p) if b, err := hex.DecodeString(id); err != nil || len(b) != 32 { panic("hlmailbox: not a 32-byte message id in hex: " + id) } dup := false for _, x := range out { if x == id { dup = true } } if !dup { out = append(out, id) } } return out, rng, false } // SetFlag sets, or re-asserts, the caller's flag on one function of a route: fn is sends, credits or payouts. ids // is what a guardian's payouts flag suspects — a comma-separated list of message ids, or a range of credits // ("heights:<from>-<to>" inclusive, or "since:<from>" for every credit the verifier makes from that height on) — // "" otherwise; a re-assertion replaces it. A watch key's flag never names ids or a range. func SetFlag(cur realm, route, fn, ids string) { who := caller(cur) seat := SeatOf(who) if seat == 0 { panic("hlmailbox: not a guardian or a watch key: " + string(who)) } if !routes.Has(route) { panic("hlmailbox: no route " + route) } if !isFn(fn) { panic("hlmailbox: the function must be sends, credits or payouts, not " + fn) } list, rng, ranged := parseIDs(ids) if (len(list) > 0 || ranged) && (fn != FnPayouts || !IsGuardianSeat(seat)) { panic("hlmailbox: only a guardian's payouts flag names ids, or a range") } flags.Set(flagKey(route, fn, seat), &flag{seat: seat, at: now(), ids: list, ranged: ranged, rng: rng}) named := strconv.Itoa(len(list)) if ranged { named = rng.String() } chain.Emit("Flag", "route", route, "fn", fn, "seat", strconv.Itoa(seat), "ids", named) } // ClearFlag clears the caller's own flag on one function of a route. It resets no meter. func ClearFlag(cur realm, route, fn string) { who := caller(cur) seat := SeatOf(who) if seat == 0 { panic("hlmailbox: not a guardian or a watch key: " + string(who)) } if _, ok := flags.Remove(flagKey(route, fn, seat)); !ok { panic("hlmailbox: no such flag of yours") } chain.Emit("Unflag", "route", route, "fn", fn, "seat", strconv.Itoa(seat)) } // ClearFlags clears every seat's flag for fn on the calling route — the void builder's andUnpause option (§5.3), // which reopens payouts in the same closure as the void. It reports whether any flag was cleared. func ClearFlags(cur realm, fn string) bool { path := mustBeRoute(cur) if !isFn(fn) { panic("hlmailbox: the function must be sends, credits or payouts, not " + fn) } n, _ := clearFns(path, []string{fn}, now()) return n > 0 } // VacateSeat removes the key in a seat if holder still has it, its flags with it (a payouts flag among them // restarting the route's meters): the void builder's unpause option retires keys in the same closure as the void // (§5.3). Only a route realm may call it. It reports whether the seat was vacated: a seat whose occupant changed // since the proposal named it, or one emptied since, is left as it is (R17), so a replacement that landed in // between is never undone by a removal that meant the key before it. func VacateSeat(cur realm, seat int, holder address) bool { mustBeRoute(cur) if seat < 1 || seat > seatCount { panic("hlmailbox: the seat must be 1 to 6") } if seats[seat] == "" || seats[seat] != holder { return false } clearSeat(seat, now()) seats[seat] = "" chain.Emit("SeatSet", "seat", strconv.Itoa(seat), "address", "") return true } // AddRoute registers a successor route realm (a route's migrate names it), so flags can be set on it too, and // records the succession both ways (AQ1): the caller's `successor`, the new route's `predecessor`, which is what // lets the successor take a message addressed to the predecessor (Process) — the burns name the predecessor until // kind 6 enrolls the successor. The path is a deployed realm's: a sub-identity's (`host#subpath`) is refused, since // the route powers are keyed by exact pkgpath (mustBeRoute) and such a path would hand them to a token its host // mints for free; a path that is a route already is refused (a chain never cycles). A route that migrated already // re-points its migration (AR2): its migrate decision names another successor while the first pulled nothing — // the route's own state refuses once page 0 moved, and a first successor that migrated on holds the lock, so it // is refused here too — and the first's registration is dropped with the flags copied to it, the new path taking // its place with the flags standing on the route now. func AddRoute(cur realm, path string) { from := mustBeRoute(cur) if !strings.HasPrefix(path, "gno.land/r/") || strings.Contains(path, "#") { panic("hlmailbox: a route is a realm under gno.land/r/, not " + path) } if routes.Has(path) { panic("hlmailbox: " + path + " is a route already") } rec := routes.Get(from).(*routeRec) if old := rec.successor; old != "" { if s := Successor(old); s != "" { panic("hlmailbox: " + from + " migrated to " + old + ", which migrated on to " + s + " already: the migration is under way and cannot be re-pointed") } if Pulled(old) { panic("hlmailbox: " + from + " migrated to " + old + ", which pulled page 0 already: the migration is under way and cannot be re-pointed") } dropRoute(from, rec, old) } rec.successor = path undrop(path) routes.Set(path, &routeRec{predecessor: from}) n := copyFlags(from, path) chain.Emit("RouteAdded", "pkgpath", path, "predecessor", from, "flags", strconv.Itoa(n)) } // dropRoute forgets a successor's registration (AR2: its predecessor re-pointed the migration before it pulled // anything): the record and every flag standing on its path. It is a route no more — nothing addressed to its // predecessor is its to take, no route power is its — but it is remembered (R35): on its predecessor's record and // as a tombstone, since Solana's token may still name it the router until the next kind 6 and a burn addressed to // it must be the line's last realm's to take, never its own. A later migrate may register it afresh (undrop). func dropRoute(from string, rec *routeRec, path string) { routes.Remove(path) var keys []string flags.Iterate(path+"|", path+"|~", func(k string, _ any) bool { keys = append(keys, k) return false }) for _, k := range keys { flags.Remove(k) } rec.dropped = append(rec.dropped, path) droppedRoutes.Set(path, from) chain.Emit("RouteDropped", "pkgpath", path, "predecessor", from, "flags", strconv.Itoa(len(keys))) } // undrop clears a path's tombstone when a migrate registers it afresh (R35): the migration re-pointed back to it // while the newer successor pulled nothing, or a later route of the line named it. It is then a route like any // other, with nothing pulled. func undrop(path string) { v := droppedRoutes.Get(path) if v == nil { return } by := v.(string) droppedRoutes.Remove(path) if r := routes.Get(by); r != nil { rec := r.(*routeRec) var kept []string for _, p := range rec.dropped { if p != path { kept = append(kept, p) } } rec.dropped = kept } chain.Emit("RouteUndropped", "pkgpath", path, "predecessor", by) } // Dropped is the predecessor whose re-point dropped the realm at path from the line (R35) — a route no more, in // nobody's chain, that neither sends (Dispatch refuses it) nor takes (Process refuses it) — or "" for a route, a // realm never registered, or one registered afresh since. func Dropped(path string) string { v := droppedRoutes.Get(path) if v == nil { return "" } return v.(string) } // Pulled is whether the successor at path pulled page 0 of its predecessor's migration, as the predecessor's Pull // noted it (NotePulled): the lock and the counters moved, so the migration is never re-pointed and a kind 6 may // enrol it (R35). False for a launch route, a successor that pulled nothing, or no route. func Pulled(path string) bool { v := routes.Get(path) if v == nil { return false } return v.(*routeRec).pulled } // NotePulled is the predecessor's note, from its Pull, that the successor its migrate registered has pulled page 0 // (R35): the lock and the counters moved with it, so the migration is under way — the route's own state refuses a // re-point from here on, and AddRoute refuses one too — and r/hlgov's kind 6 may enrol the successor: before it a // kind 6 could name a route a re-point then drops, leaving Solana's token minting for a realm in nobody's chain. // Only the route whose successor it is notes it; a second note (a second Pull of page 0) changes nothing. func NotePulled(cur realm, path string) { from := mustBeRoute(cur) rec := routes.Get(from).(*routeRec) if rec.successor != path { panic("hlmailbox: " + path + " is not " + from + "'s successor") } sv := routes.Get(path).(*routeRec) if !sv.pulled { sv.pulled = true chain.Emit("RoutePulled", "pkgpath", path, "predecessor", from) } } // copyFlags gives the route at `to` a copy of every flag standing on `from` — the seat, the time it was set, the // ids or the range it names — keyed by the new path (R27): the pause follows the lock and the claims to the // successor, whose Claim and Process ask their own path's flags, and clears there as any flag does — by the // seat, by GovDAO's unpause, or with the seat's removal. It reports how many were copied. func copyFlags(from, to string) (n int) { var keys []string var fs []*flag flags.Iterate(from+"|", from+"|~", func(k string, v any) bool { keys = append(keys, k) fs = append(fs, v.(*flag)) return false }) for i, k := range keys { f := fs[i] fn := strings.Split(k, "|")[1] 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}) n++ } return n } // Predecessor is the route whose migrate registered path as its successor; "" for a launch route, or no route. func Predecessor(path string) string { v := routes.Get(path) if v == nil { return "" } return v.(*routeRec).predecessor } // Successor is the route path's migrate registered; "" while it never migrated, or for no route. func Successor(path string) string { v := routes.Get(path) if v == nil { return "" } return v.(*routeRec).successor } // Enrolled is the block height and unix time r/hlgov last dispatched a kind 6 enrolling the route at path as the // token's router on Solana (NoteEnrolled); 0, 0 if never, or for no route. A route's refund builder waits 7 days // after it (AR1). func Enrolled(path string) (height, at int64) { v := routes.Get(path) if v == nil { return 0, 0 } r := v.(*routeRec) return r.enrolled, r.enrolledAt } // mustBeHlgov is the caller check of what r/hlgov alone does here: its exact pkgpath, read from the live cur. func mustBeHlgov(cur realm) { if !cur.IsCurrent() { panic("hlmailbox: that realm value is not live; pass your own cur") } if p := cur.Previous().PkgPath(); p != hlgovPath { panic("hlmailbox: only " + hlgovPath + " notes an enrolment, not " + p) } } // NoteEnrolled is r/hlgov's note, from its kind-6 closure, that it dispatched a decision enrolling the route at // path as the token's router on Solana at this block height (AR1): the route's refund builder and its apply read // it (Enrolled) and refuse a refund until the note is 14 days old (R37: a week for the delivery, a week for // Solana's own clock), since a refunded send stays deliverable on Solana until the predecessor's router is // replaced and a refund that lands first lets one stranger's Reemit and delivery mint it twice. The latest note // counts: a later kind 6 for the route restarts the wait; a cancelled one clears it (ClearEnrolled). Necessary, // not sufficient — the realm cannot see Solana; hlwatch reads the token's enrolled router there and alarms a // refund proposal whose successor the token has not enrolled. func NoteEnrolled(cur realm, path string, height int64) { mustBeHlgov(cur) v := routes.Get(path) if v == nil { panic("hlmailbox: no route " + path) } if height < 1 { panic("hlmailbox: not a block height: " + strconv.FormatInt(height, 10)) } r := v.(*routeRec) r.enrolled, r.enrolledAt = height, now() chain.Emit("RouteEnrolled", "pkgpath", path, "height", strconv.FormatInt(height, 10)) } // ClearEnrolled is r/hlgov's word, from its cancel closure, that the kind 6 whose note stands on the route at path // was cancelled (R37, AS2): the note goes, so the route's refund gate reads "no kind 6" again and its clock // restarts with a re-proposal — before, a cancelled, lost or held kind 6 left the note standing and the gate // opened on the realm's clock alone while the predecessor was still the enrolled router. r/hlgov's exact pkgpath // alone, for a route; a route with no note is left as it is. func ClearEnrolled(cur realm, path string) { mustBeHlgov(cur) v := routes.Get(path) if v == nil { panic("hlmailbox: no route " + path) } r := v.(*routeRec) if r.enrolled == 0 { return } r.enrolled, r.enrolledAt = 0, 0 chain.Emit("RouteEnrolmentCleared", "pkgpath", path) } // successorOf is whether the route at path succeeds the route whose address, as 32 bytes, is recipient — along the // chain of migrations, at any distance (AQ1): a burn addressed to a migrated route is taken by the successor that // holds its lock. A successor a predecessor's re-point dropped counts as that predecessor's (R35): a burn made // while Solana still named it the router is the line's live last realm's to take, never its own. A caller that is // no route, or whose chain never reaches the recipient, is not. func successorOf(path string, recipient [32]byte) bool { for i := 0; i < maxChain && path != ""; i++ { v := routes.Get(path) if v == nil { return false } path = v.(*routeRec).predecessor if path == "" { return false } if mustBytes32(chain.PackageAddress(path)) == recipient { return true } if pv := routes.Get(path); pv != nil { for _, d := range pv.(*routeRec).dropped { if mustBytes32(chain.PackageAddress(d)) == recipient { return true } } } } return false } // AnnounceSuccessor is how a realm offers itself as a route's successor (R25): called once from that realm's own // code (its init, usually), naming the route it succeeds — GNOT or BUBBLE (AQ5: a migrate names only a successor // announced for its own route) — it is kept under the caller's exact pkgpath, which a route's migrate builder and // its closure require of the path they name — so the lock never goes to a path with no realm behind it (a typo, // or an undeployed path whose namespace owner could deploy anything there and pull the lock). Only an /r/ realm, // with its own cur: a sub-realm identity (host#subpath, a token a realm mints for free) is refused as in // RegisterVerifier, so every entry costs a package deployment and the listing (render.gno) is bounded. Nothing is // ever withdrawn; GovDAO reads the successor's source before it votes, as before. func AnnounceSuccessor(cur realm, symbol string) { if !cur.IsCurrent() { panic("hlmailbox: that realm value is not live; pass your own cur") } path := cur.Previous().PkgPath() if !strings.HasPrefix(path, "gno.land/r/") { panic("hlmailbox: only a realm under gno.land/r/ announces itself as a successor, not " + path) } if cur.Previous().Subpath() != "" { panic("hlmailbox: a realm announces itself with its own cur, not a sub-identity (" + path + ")") } if symbol != SymbolGNOT && symbol != SymbolBUBBLE { panic("hlmailbox: the route a successor announces for is GNOT or BUBBLE, not " + symbol) } if successors.Has(path) { panic("hlmailbox: " + path + " announced itself already") } successors.Set(path, symbol) chain.Emit("SuccessorAnnounced", "pkgpath", path, "route", symbol) } // SuccessorAnnounced is whether a realm at this pkgpath announced itself as a possible successor: what a route's // migrate builder and its closure require of the path they name (R25). func SuccessorAnnounced(path string) bool { return successors.Has(path) } // SuccessorFor is the route — GNOT or BUBBLE — a realm at this pkgpath announced itself as a successor of (AQ5); // "" if it never announced itself. A route's migrate builder and its closure refuse another route's successor. func SuccessorFor(path string) string { v := successors.Get(path) if v == nil { return "" } return v.(string) } // RouteSymbol is the token a realm's route is for (R36): the launch paths' compiled symbols; for every other // realm the route it announced itself a successor of (SuccessorFor — a registered successor always announced, // AQ5); "" for one that never did. r/hlgov's kind 6 compares it with the token it names, so a slip can never // enrol one token's route as the other's router. func RouteSymbol(path string) string { switch path { case launchRouteGNOT: return SymbolGNOT case launchRouteBUBBLE: return SymbolBUBBLE } return SuccessorFor(path) } // clearFns removes every seat's flag for the named functions on a route; payouts says whether a standing payouts // flag went with them, in which case the route's meters restart (the epoch bumps). func clearFns(path string, fns []string, t int64) (n int, payouts bool) { for _, fn := range fns { var keys []string flags.Iterate(flagPrefix(path, fn), flagPrefix(path, fn)+"~", func(k string, _ any) bool { keys = append(keys, k) return false }) for _, k := range keys { flags.Remove(k) n++ if fn == FnPayouts { payouts = true } } } if payouts { bump(path, t) } return n, payouts } // clearSeat removes every flag a seat holds, on every route, bumping the epoch of each route where a payouts // flag stood. func clearSeat(seat int, t int64) (n int) { suffix := "|" + strconv.Itoa(seat) var keys []string flags.Iterate("", "", func(k string, v any) bool { if strings.HasSuffix(k, suffix) && v.(*flag).seat == seat { keys = append(keys, k) } return false }) for _, k := range keys { flags.Remove(k) n++ parts := strings.Split(k, "|") if parts[1] == FnPayouts { bump(parts[0], t) } } return n } func bump(path string, t int64) { v := routes.Get(path) if v == nil { return } r := v.(*routeRec) r.epoch++ r.epochAt = t chain.Emit("PayoutsReset", "route", path, "epoch", strconv.FormatInt(r.epoch, 10)) }
  4. #4gnomod.toml
  5. #5module = "gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hlmailbox" gno = "0.9"
  6. #6gov.gno
  7. #7package hlmailbox import ( "chain" "chain/runtime" "errors" "strconv" "strings" "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/decisions/v0" "gno.land/r/gov/dao" ) // The builders (bridge/PLAN.md §5.3): the only way a setting of this realm changes is a decision made with one of // the New…Request functions — before the governor yields, the governor's own `maketx run` script (M1k): // // hlmailbox.Govern(cross(cur), hlmailbox.NewSetVerifierRequest(cross(cur), origin, path)) // // and after it a GovDAO proposal from a member's: // // req := hlmailbox.NewSetVerifierRequest(cross(cur), origin, path) // dao.MustCreateProposal(cross(cur), req.Proposal()) // // A builder takes no free text: it checks its parameters, writes them as the proposal's description, and returns // a Request whose closure runs when the decision is made — the one closure, whether the governor runs it through // Govern or GovDAO's proxy runs it when the proposal executes. The closure checks who decides (mustBeGovernor: the // governor's own call until the yield, gno.land/r/gov/dao's proxy after), returns an error only when the request // is past its 14-day deadline (GovDAO then denies the proposal for good; Govern panics with it) and panics on // anything else (a proposal survives for a retry), gives the decision a number, and decides the at-once test // against the live state: a removal or an unpause applies at once; a swap or an addition is queued 7 days and // anyone applies it with Execute(id) after that. Launch is the one-shot that makes the realms live; it carries the // same 14-day deadline (a passed launch nobody executed in 14 days lapses and is re-proposed, never a standing // trap); before it every builder but Launch and cancel is refused. // Request is what a builder returns (M1k): the proposal GovDAO's proxy executes after the yield (Proposal), and // the same decision the governor runs directly before it (Govern). The closure is one; only who may run it // changes with the yield. A Request comes from a builder alone — the type is this realm's and the closure // unexported, so no other code can build one or change what it decides (the construction and conversion guards of // the interrealm spec, §3.2 and §8.2) — and a stranger holding one can only hand it back to Govern, which refuses // everyone but the governor. type Request struct { proposal dao.ProposalRequest text string decide func(cur realm) error } // Proposal is the request as a GovDAO proposal: `dao.MustCreateProposal(cross(cur), req.Proposal())`. func (r Request) Proposal() dao.ProposalRequest { return r.proposal } // Description is the proposal's: the parameters as the builder wrote them. func (r Request) Description() string { return r.text } type ( launchParams struct{} setVerifierParams struct { origin uint32 path string } rotateSignersParams struct { keys []Key rule Rule // A queued rotation keeps the signer set and rule it was decided against; Execute applies it only while // they still stand, so a removal made in between (a compromised key cut at once) can never be undone by // an older proposal landing on its 7th day — GovDAO proposes afresh against the set as it is. oldKeys []Key oldRule Rule } rotateSeatsParams struct { seats []int addrs []address // "" removes the seat held []address // each seat's holder when the proposal was made: a removal of a seat whose occupant changed since is skipped (R21, as the void's and the unpause's, R17) before [seatCount + 1]address // the six seats as they stood when the proposal executed (a queued change applies only while they stand) } unpauseParams struct { route string fns []string removeSeat int removeAddr address // the seat's holder when the proposal was made: the seat is vacated only while it still holds it (R17) } cancelParams struct{ id int64 } ) // who names a previous realm in a refusal: its pkgpath, or a user's address. func who(path string, a address) string { if path != "" { return path } return string(a) } // mustBeGovernor is a decision's identity check (M1k), read from the closure's own live cur: until the yield the // governor's own call — the compiled address, by its own transaction (cur.Previous() a user realm: a MsgCall's, or // a MsgRun script's ephemeral realm, whose address is the signer's; never another realm), which is Govern's // non-crossing call of the closure — and gno.land/r/gov/dao's proposals refused; after it, or when no governor // was compiled, the proxy's exact pkgpath, as before M1k, and the governor refused like anyone. A refusal is a // panic: a proposal made before the yield survives for a retry after it, within its 14 days. func mustBeGovernor(cur realm) { prev := cur.Previous() if decisions.DAOGoverns(launchGovernor, yielded) { if p := prev.PkgPath(); p != decisions.DAOPath { panic("hlmailbox: only " + decisions.DAOPath + " executes a decision, not " + who(prev.PkgPath(), prev.Address())) } return } if !prev.IsUser() || prev.Address() != launchGovernor { panic("hlmailbox: until the governor yields, " + string(launchGovernor) + " alone decides, by Govern from its own key: not " + who(prev.PkgPath(), prev.Address()) + " (a GovDAO proposal is refused until the yield)") } } func mustBeLive() { if !live { panic("hlmailbox: not launched: before the Launch decision only Launch and cancel are proposed") } } // propose wraps a builder's effect in the closure every builder shares. apply runs at the decision with the time // the request was made and the time now; it records the decision. func propose(cur realm, kind, text string, apply func(made, t int64)) Request { if !cur.IsCurrent() { panic("hlmailbox: that realm value is not live; pass your own cur") } made := now() cb := func(cur realm) error { mustBeGovernor(cur) t := now() if err := decisions.Expired(made, t); err != nil { return err } apply(made, t) return nil } return request(cur, kind, text, cb) } // request is a builder's Request: the proposal around the closure for GovDAO's path, the closure itself for the // governor's. func request(cur realm, kind, text string, cb func(cur realm) error) Request { return Request{proposal: dao.NewProposalRequest("hlmailbox: "+kind, text, dao.NewSimpleExecutor(0, cur, cb, text)), text: text, decide: cb} } // Govern is the governor's way in before the yield (M1k): the governor — the compiled launchGovernor, by its own // transaction (a MsgCall, or the MsgRun script that makes the request in the same transaction; never through // another realm) — makes a builder's decision directly, the same closure GovDAO's proxy runs after the yield: the // same checks, the same at-once test, the same 7-day queue and 14-day deadline, the same record and events. The // closure is called non-crossing, in this frame, so it reads the governor from cur.Previous() as it reads the // proxy after the yield. Refused for anyone else, and for everyone once the governor yielded (or when none was // compiled): GovDAO's proposals decide then. A request past its 14 days is refused (the deadline error, as a // proposal's denial) and made afresh. func Govern(cur realm, r Request) { if !cur.IsCurrent() { panic("hlmailbox: that realm value is not live; pass your own cur") } if r.decide == nil { panic("hlmailbox: not a builder's request") } if decisions.DAOGoverns(launchGovernor, yielded) { panic("hlmailbox: " + governedBy() + ": a decision is a GovDAO proposal, never Govern") } mustBeGovernor(cur) if err := r.decide(cur); err != nil { panic("hlmailbox: " + err.Error()) } } // governedBy says why Govern is closed: the yield's height, or no governor compiled. func governedBy() string { if launchGovernor == "" { return "no governor was compiled, " + decisions.DAOPath + " governs from the first block" } return "the governor yielded at height " + strconv.FormatInt(yielded, 10) + ", " + decisions.DAOPath + " governs since" } // Yield is the governor's one-way hand-over to GovDAO (M1k): from this block gno.land/r/gov/dao's proposals are // the only way a decision is made on every bridge realm — each reads Yielded here before it accepts one, so no // realm is ever yielded apart from the rest — and Govern is closed for good. The governor alone, by its own // transaction; refused when no governor was compiled (GovDAO governs from the first block) and a second time. A // decision the governor queued still applies by Execute(id) after its 7 days, or GovDAO cancels it. func Yield(cur realm) { caller(cur) if launchGovernor == "" { panic("hlmailbox: no governor was compiled: " + decisions.DAOPath + " governs from the first block") } if yielded != 0 { panic("hlmailbox: the governor yielded at height " + strconv.FormatInt(yielded, 10) + " already") } prev := cur.Previous() if !prev.IsUser() || prev.Address() != launchGovernor { panic("hlmailbox: only the governor " + string(launchGovernor) + " yields, not " + who(prev.PkgPath(), prev.Address())) } yielded = runtime.ChainHeight() // never 0: the chain's first executing block is height 1 chain.Emit("Yielded", "governor", string(launchGovernor), "height", strconv.FormatInt(yielded, 10)) } func queue(kind, text string, params any, made, t int64) *decisions.Decision { d := ledger.Record(kind, text, params, made, t, t+decisions.Delay) d.Note = "queued 7 days" chain.Emit("Decision", "id", strconv.FormatInt(d.ID, 10), "kind", kind, "state", "queued") return d } func atOnce(kind, text string, params any, made, t int64, note string) *decisions.Decision { d := ledger.Record(kind, text, params, made, t, t) ledger.Apply(d, t, "at once: "+note) chain.Emit("Decision", "id", strconv.FormatInt(d.ID, 10), "kind", kind, "state", "applied") return d } // ------------------------------------------------------------- launch -- // NewLaunchRequest is the launch's builder — the governor's decision like any other (M1k), the launch vote's once // the governor has yielded: its execution makes the realms live, once, with the compiled values. It carries the // 14-day deadline every builder carries: a passed launch nobody executed within 14 days of its proposal returns // the deadline error (GovDAO denies it for good) and the launch is proposed again, so a forgotten vote is never a // standing trap. A second execution errors. func NewLaunchRequest(cur realm) Request { if !cur.IsCurrent() { panic("hlmailbox: that realm value is not live; pass your own cur") } if live { panic("hlmailbox: launched already") } made := now() text := "launch: the compiled values go live — verifier " + launchVerifier + " for origin " + u32(launchOrigin) + ", signers " + launchSigners + " under the rule " + Rule{Threshold: launchThreshold}.String() + ", routes " + launchRouteGNOT + " and " + launchRouteBUBBLE + ", seats " + seatsText() cb := func(cur realm) error { mustBeGovernor(cur) t := now() if err := decisions.Expired(made, t); err != nil { return err } if live { return errors.New("hlmailbox: launched already") } live, launchedAt = true, t atOnce("launch", text, launchParams{}, made, t, "live") chain.Emit("Launch", "at", strconv.FormatInt(t, 10)) return nil } return request(cur, "launch", text, cb) } func seatsText() string { parts := make([]string, 0, seatCount) for i := 1; i <= seatCount; i++ { parts = append(parts, strconv.Itoa(i)+":"+string(seats[i])) } return strings.Join(parts, ",") } // ------------------------------------------------------------- verifier -- // NewSetVerifierRequest activates a registered verifier for an origin, 7 days after the proposal executes. func NewSetVerifierRequest(cur realm, origin uint32, path string) Request { mustBeLive() if origin == localDomain { panic("hlmailbox: this chain's own domain has no verifier") } if !verifiers.Has(path) { panic("hlmailbox: no verifier registered at " + path) } if ActiveVerifier(origin) == path { panic("hlmailbox: " + path + " is the active verifier for origin " + u32(origin) + " already") } text := "set verifier for origin " + u32(origin) + " to " + path return propose(cur, "set verifier", text, func(made, t int64) { mustBeLive() if !verifiers.Has(path) { panic("hlmailbox: no verifier registered at " + path) } queue("set verifier", text, setVerifierParams{origin, path}, made, t) }) } // -------------------------------------------------------------- signers -- // parseSigners reads a signer set and its rule: keys in hex, comma separated; a threshold, or both groups (comma // separated hex, subsets of the keys) with threshold 0. func parseSigners(keys string, threshold int, groupA, groupB string) ([]Key, Rule) { ks := parseKeys(keys) if len(ks) == 0 { panic("hlmailbox: no signers") } a, b := parseKeys(groupA), parseKeys(groupB) if (len(a) == 0) != (len(b) == 0) { panic("hlmailbox: name both groups or neither") } if len(a) > 0 { if threshold != 0 { panic("hlmailbox: a two-group rule takes no threshold") } if !subset(a, ks) || !subset(b, ks) { panic("hlmailbox: every group member must be a signer") } for _, k := range a { if indexOf(b, k) >= 0 { panic("hlmailbox: a key in both groups: " + hx(k[:])) } } return ks, Rule{GroupA: a, GroupB: b} } if threshold < 1 || threshold > len(ks) { panic("hlmailbox: the threshold must be between 1 and the number of signers") } return ks, Rule{Threshold: threshold} } // pruned is the live rule with every key absent from ks dropped from both groups: what a pure removal leaves of // the rule. A threshold is not pruned (§5.3: a 2-of-3 rule is still met by the 2 that remain after one removal); // a group is (one of A and one of B is still met by the members that remain). Removing a partner — the only // member of its group — leaves a group empty, which no proposal names, so that removal is never at once. func pruned(ks []Key) Rule { r := Rule{Threshold: signerRule.Threshold} for _, k := range signerRule.GroupA { if indexOf(ks, k) >= 0 { r.GroupA = append(r.GroupA, k) } } for _, k := range signerRule.GroupB { if indexOf(ks, k) >= 0 { r.GroupB = append(r.GroupB, k) } } return r } // NewRotateSignersRequest sets the signer set and its rule together (§5.3): the keys in hex, comma separated, in // the order signatures name them; a threshold, or §11.13's two groups (one of each must sign; threshold 0). It // applies at once only as a pure removal — the new set a subset of the old, under the live rule pruned of the // removed keys (the same threshold; the groups less the keys that leave), which the active verifier says the // remaining set can still meet (Satisfiable, decided when the proposal executes); any other change, a lowered // threshold among them, is queued 7 days, and a queued rotation applies only while the signer set and rule are // still those it was decided against. func NewRotateSignersRequest(cur realm, keys string, threshold int, groupA, groupB string) Request { mustBeLive() ks, rule := parseSigners(keys, threshold, groupA, groupB) if sameKeys(ks, signerKeys) && rule.equal(signerRule) { panic("hlmailbox: nothing changes") } // a pure removal is checked against the active verifier's Satisfiable when it is made too (K19): one that // would leave the rule unsatisfiable is nonsense, refused early — at execution the test is made again, and a // removal it fails then (another landed in between) is queued, never refused (§5.3) if rule.sameAs(pruned(ks)) && len(ks) < len(signerKeys) && subset(ks, signerKeys) && !satisfiable(ks) { panic("hlmailbox: the active verifier says the signers that remain cannot meet the rule: a removal never leaves the rule unsatisfiable") } text := "rotate signers to " + keysHex(ks) + " under the rule " + rule.String() return propose(cur, "rotate signers", text, func(made, t int64) { mustBeLive() p := rotateSignersParams{keys: ks, rule: rule} if rule.sameAs(pruned(ks)) && len(ks) < len(signerKeys) && subset(ks, signerKeys) && satisfiable(ks) { signerKeys, signerRule = ks, rule atOnce("rotate signers", text, p, made, t, "a removal the rule survives") chain.Emit("SignersSet", "count", strconv.Itoa(len(ks)), "rule", rule.String()) return } p.oldKeys, p.oldRule = Signers() d := queue("rotate signers", text, p, made, t) d.Note += "; applies only while the signer set and rule stand as they do now" }) } // ---------------------------------------------------------------- seats -- // parseSeatChanges reads "seat:address" pairs, comma separated; an empty address ("1:" or "1:-") vacates the seat. func parseSeatChanges(changes string) ([]int, []address) { if strings.TrimSpace(changes) == "" { panic("hlmailbox: no changes") } var ns []int var as []address g := 0 for _, p := range strings.Split(changes, ",") { i := strings.Index(p, ":") if i < 0 { panic("hlmailbox: a change is seat:address, not " + p) } n, err := strconv.Atoi(strings.TrimSpace(p[:i])) if err != nil || n < 1 || n > seatCount { panic("hlmailbox: the seat must be 1 to 6, not " + p[:i]) } for _, x := range ns { if x == n { panic("hlmailbox: seat " + strconv.Itoa(n) + " named twice") } } a := address(strings.TrimSpace(p[i+1:])) if a == "-" { a = "" } if a != "" && !a.IsValid() { panic("hlmailbox: not an address: " + string(a)) } if a == "" && seats[n] == "" { panic("hlmailbox: seat " + strconv.Itoa(n) + " is empty already") } if a != "" && seats[n] == a { panic("hlmailbox: seat " + strconv.Itoa(n) + " holds " + string(a) + " already") } if a != "" && SeatOf(a) != 0 { panic("hlmailbox: " + string(a) + " holds seat " + strconv.Itoa(SeatOf(a)) + " already") } for j, x := range as { if x != "" && x == a { panic("hlmailbox: " + string(a) + " named for seats " + strconv.Itoa(ns[j]) + " and " + strconv.Itoa(n)) } } if IsGuardianSeat(n) { g++ } ns = append(ns, n) as = append(as, a) } if g > 2 { panic("hlmailbox: two guardians at most per proposal") } return ns, as } func seatChangesText(ns []int, as []address) string { parts := make([]string, len(ns)) for i, n := range ns { if as[i] == "" { parts[i] = "remove seat " + strconv.Itoa(n) + " (" + string(seats[n]) + ")" } else { parts[i] = "set seat " + strconv.Itoa(n) + " to " + string(as[i]) } } return strings.Join(parts, ", ") } // NewRotateSeatsRequest replaces or vacates guardian and watch-key seats: changes is "seat:address" pairs, comma // separated, an empty address vacating the seat; two guardians at most per proposal; a seat's current key is // refused. Removals alone apply at once; any replacement queues the whole proposal 7 days, and a queued one // applies only while the six seats still stand as they did when it was decided. A removed or replaced key's // flags clear with it. A removal means the key the seat holds when the proposal is made (the description names // it): a seat whose occupant changed by execution — a replacement landed in between — is skipped and the note says // so (R21), the proposal spent if nothing was left to remove. func NewRotateSeatsRequest(cur realm, changes string) Request { mustBeLive() ns, as := parseSeatChanges(changes) held := make([]address, len(ns)) for i, n := range ns { held[i] = seats[n] } text := "rotate seats: " + seatChangesText(ns, as) return propose(cur, "rotate seats", text, func(made, t int64) { mustBeLive() p := rotateSeatsParams{seats: ns, addrs: as, held: held} removals := true for _, a := range as { if a != "" { removals = false } } if removals { vacated, skipped := applySeats(p, t) if len(vacated) == 0 { d := ledger.Record("rotate seats", text, p, made, t, t) ledger.Spend(d, "spent: nothing to remove, "+seatsNote(nil, skipped)) chain.Emit("Decision", "id", strconv.FormatInt(d.ID, 10), "kind", "rotate seats", "state", "spent") return } atOnce("rotate seats", text, p, made, t, "removals: "+seatsNote(vacated, skipped)) return } p.before = seats d := queue("rotate seats", text, p, made, t) d.Note += "; applies only while the seats stand as they do now" }) } // applySeats makes a proposal's seat changes: a replacement as named; a removal only while the seat still holds the // key the proposal was made against (R21). It reports the seats vacated and the removals skipped. func applySeats(p rotateSeatsParams, t int64) (vacated, skipped []int) { for i, n := range p.seats { a := p.addrs[i] if a == "" { if seats[n] == "" || seats[n] != p.held[i] { skipped = append(skipped, n) continue } clearSeat(n, t) seats[n] = "" vacated = append(vacated, n) chain.Emit("SeatSet", "seat", strconv.Itoa(n), "address", "") continue } if SeatOf(a) != 0 && SeatOf(a) != n { panic("hlmailbox: " + string(a) + " holds seat " + strconv.Itoa(SeatOf(a)) + " now") } if seats[n] != "" { clearSeat(n, t) } seats[n] = a chain.Emit("SeatSet", "seat", strconv.Itoa(n), "address", string(a)) } return vacated, skipped } // seatsNote says which seats a proposal vacated and which it skipped. func seatsNote(vacated, skipped []int) string { var parts []string if len(vacated) > 0 { parts = append(parts, "seats "+seatList(vacated)+" vacated") } if len(skipped) > 0 { parts = append(parts, "seats "+seatList(skipped)+" skipped (the occupant changed since the proposal was made)") } return strings.Join(parts, ", ") } func seatList(ns []int) string { parts := make([]string, len(ns)) for i, n := range ns { parts[i] = strconv.Itoa(n) } return strings.Join(parts, ",") } // -------------------------------------------------------------- unpause -- func parseFns(fns string) []string { var out []string for _, p := range strings.Split(fns, ",") { fn := strings.TrimSpace(p) if !isFn(fn) { panic("hlmailbox: the functions are sends, credits and payouts, not " + fn) } for _, x := range out { if x == fn { panic("hlmailbox: " + fn + " named twice") } } out = append(out, fn) } return out } // NewUnpauseRequest clears every seat's flag for the named functions of a route (sends, credits, payouts; comma // separated), and may vacate one seat in the same stroke (removeSeat, 0 for none — one seat, so §5.3's cap of two // guardians per proposal holds by construction; the void builder's unpause option, which takes a list, counts // them). The seat's holder now is what the proposal means and is recorded with it: a seat whose occupant changed // by execution — a replacement landed in between — is skipped, and the note says so (R17). It applies at once; // the builder refuses one that would clear nothing now, and an executed one that finds nothing to clear is spent // (recorded, no effect, no meter reset), never queued. func NewUnpauseRequest(cur realm, routePath, fns string, removeSeat int) Request { mustBeLive() if !routes.Has(routePath) { panic("hlmailbox: no route " + routePath) } list := parseFns(fns) var removeAddr address if removeSeat != 0 { if removeSeat < 1 || removeSeat > seatCount { panic("hlmailbox: the seat must be 1 to 6") } if removeAddr = seats[removeSeat]; removeAddr == "" { panic("hlmailbox: seat " + strconv.Itoa(removeSeat) + " is empty") } } clears := false for _, fn := range list { if Paused(routePath, fn) { clears = true } } if !clears { panic("hlmailbox: no flag stands on " + fns + " of " + routePath + ": nothing to clear") } text := "unpause " + strings.Join(list, ",") + " on " + routePath if removeSeat != 0 { text += ", removing seat " + strconv.Itoa(removeSeat) + " (" + string(removeAddr) + ")" } return propose(cur, "unpause", text, func(made, t int64) { mustBeLive() p := unpauseParams{route: routePath, fns: list, removeSeat: removeSeat, removeAddr: removeAddr} n, payouts := clearFns(routePath, list, t) removed, skipped := false, false if removeSeat != 0 { if seats[removeSeat] == removeAddr { clearSeat(removeSeat, t) seats[removeSeat] = "" removed = true chain.Emit("SeatSet", "seat", strconv.Itoa(removeSeat), "address", "") } else { skipped = true } } seatNote := "" if removed { seatNote = ", seat " + strconv.Itoa(removeSeat) + " vacated" } else if skipped { seatNote = ", seat " + strconv.Itoa(removeSeat) + " skipped (its occupant changed since the proposal was made)" } if n == 0 && !removed { d := ledger.Record("unpause", text, p, made, t, t) ledger.Spend(d, "spent: nothing to clear"+seatNote) chain.Emit("Decision", "id", strconv.FormatInt(d.ID, 10), "kind", "unpause", "state", "spent") return } note := strconv.Itoa(n) + " flags cleared" if payouts { note += ", meters restart" } atOnce("unpause", text, p, made, t, note+seatNote) }) } // --------------------------------------------------------------- cancel -- // NewCancelRequest cancels a queued decision of this realm, at once. It is the one builder proposed before Launch. func NewCancelRequest(cur realm, id int64) Request { d := ledger.Get(id) if d == nil || !d.Pending() { panic("hlmailbox: decision " + strconv.FormatInt(id, 10) + " is not pending") } text := "cancel decision " + strconv.FormatInt(id, 10) + " (" + d.Text + ")" return propose(cur, "cancel", text, func(made, t int64) { if _, err := ledger.Cancel(id, t); err != nil { panic("hlmailbox: " + err.Error()) } atOnce("cancel", text, cancelParams{id}, made, t, "cancelled") }) } // -------------------------------------------------------------- execute -- // Execute applies a queued decision whose 7 days have passed. Anyone may call it; the relayers do. A queued // rotation of signers or seats applies only while the set it was decided against still stands: one that finds // the signers or the seats changed in between panics (it stays pending; GovDAO cancels it and proposes afresh). func Execute(cur realm, id int64) { caller(cur) t := now() d, err := ledger.Due(id, t) if err != nil { panic("hlmailbox: " + err.Error()) } note := "applied after the delay" switch p := d.Params.(type) { case setVerifierParams: if !verifiers.Has(p.path) { panic("hlmailbox: no verifier registered at " + p.path) } active.Set(domainKey(p.origin), p.path) chain.Emit("VerifierSet", "origin", u32(p.origin), "pkgpath", p.path) case rotateSignersParams: if !sameKeys(signerKeys, p.oldKeys) || !signerRule.equal(p.oldRule) { panic("hlmailbox: the signer set or rule changed since decision " + strconv.FormatInt(id, 10) + " was decided: cancel it and propose the rotation against the set as it stands") } signerKeys, signerRule = p.keys, p.rule chain.Emit("SignersSet", "count", strconv.Itoa(len(p.keys)), "rule", p.rule.String()) case rotateSeatsParams: if p.before != seats { panic("hlmailbox: the seats changed since decision " + strconv.FormatInt(id, 10) + " was decided: cancel it and propose the change against the seats as they stand") } if _, skipped := applySeats(p, t); len(skipped) > 0 { note += "; " + seatsNote(nil, skipped) } default: panic("hlmailbox: decision " + strconv.FormatInt(id, 10) + " is not one that waits") } ledger.Apply(d, t, note) chain.Emit("Decision", "id", strconv.FormatInt(id, 10), "kind", d.Kind, "state", "applied") }
  8. #8mailbox.gno
  9. #9// Package hlmailbox is the gno.land end of the bridge's message layer (bridge/PLAN.md §5.1): realms send messages to // other chains through it and take the messages other chains send them from it. Messages, ids and the merkle tree // are Hyperlane's byte for byte (gno.land/p/…/hyperlane/v0), so the Solana side runs Hyperlane's own programs. // // Out. Dispatch takes a message from the realm that calls it, numbers it, puts its id in the merkle tree, keeps the // message, and emits the Dispatch event the Solana light client proves from LastResultsHash (§13: id, nonce, origin, // destination, sender, recipient, hash). Reemit emits it again from a fresh block, by anyone. // // In. A recipient realm pulls its own messages: it calls Process with the message and the verifier's metadata; // Process checks the caller is the recipient, that the message is for this chain and was never delivered, marks it // delivered, and only then asks the active verifier — a realm that registered itself here, chosen by GovDAO by // pkgpath — whether the message is genuine; a verifier's error is a panic, so the transaction and the mark revert // together and a refused message stays deliverable. // // Who may change what. Nobody holds an admin key: every setting moves by a decision made with this realm's // builders (gov.gno), at once or after 7 days (§5.3), and nothing but Launch and cancel before the one-shot // Launch. Who decides is the governor's (M1k): the compiled launchGovernor alone, by its own call through Govern, // until it yields (Yield, once, for every bridge realm at once), and GovDAO's proposals from then on — or from the // first block, when no governor was compiled. The guardians and watch keys (flags.gno) can only pause: three flags // per key per route — sends, credits, payouts — which the routes ask about with Paused. Every launch value is // compiled in below and written by init. package hlmailbox import ( "chain" "chain/runtime" "crypto/sha256" "encoding/hex" "strconv" "strings" "gno.land/p/nt/avl/v0" "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/decisions/v0" hl "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hyperlane/v0" ) const ( namespace = "gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/" pkgPath = namespace + "hlmailbox" // must match gnomod.toml // DomainMainnet and DomainTest are gno.land's Hyperlane domain ids, "GnoL" and "GnoT" in ASCII; nobody in the // Hyperlane registry holds either (2026-10-10). gnoland-1 is mainnet; every other chain is a test chain. DomainMainnet = uint32(1198419788) DomainTest = uint32(1198419796) mainnetChainID = "gnoland-1" // SolanaMainnet and SolanaLocal are the destination's domains: Solana's own on mainnet; on any other gno chain // the one the local test validator and the e2e run use (Hyperlane's "solanatestnet"). Devnet is 1399811151. SolanaMainnet = uint32(1399811149) SolanaLocal = uint32(1399811150) // The launch values (§5.3): the active verifier, the signers and their rule, the routes. The signer keys below // are PLACEHOLDERS (the three test signers of the vectors) until the operator's three machines exist; the // launch vote reads the real ones in the deployed source. bridge/local/e2e.sh swaps them for its run's keys. launchVerifier = namespace + "hlverifier" launchThreshold = 2 launchSigners = "e2c2b621677535e5b2a0a92f37aec61e7ce0397cf1cf713f91a18227eafec2ef,d837b6369a315b6d26babddc0af241d8908a1809481b29e98365965cf8301689,c0b2dc0da05eb6ee67b68068611a72858f3093c79e8d90589ff2ac1f0ea4da26" launchRouteGNOT = namespace + "hlgnot" launchRouteBUBBLE = namespace + "hlbubble" hlgovPath = namespace + "hlgov" // r/hlgov: the one realm that notes a route's enrolment on Solana (NoteEnrolled, AR1) maxSigners = 255 // a signature names its signer in one byte ) // launchGovernor is the governor (M1k, §5.3): the one address whose own direct call decides — through Govern, the // same closures, checks, queues and delays a GovDAO proposal's execution runs — until it yields (Yield), from // which block GovDAO's proposals decide and nothing else; "" means GovDAO from the first block. Every bridge // realm compiles the same address and refuses to deploy with another (its init reads Governor here), and reads // Yielded here, so no realm is ever governed apart from the rest. A PLACEHOLDER (testutils.TestAddress("hl- // governor")) until the launch notice names the real key; bridge/local/e2e.sh swaps it for gnodev's test1 in all // six realms. Its own line, so one sed on `^const launchGovernor = address(…)` finds it in every realm. const launchGovernor = address("g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6") var ( localDomain = domainOf(runtime.ChainID()) launchOrigin = solanaOf(runtime.ChainID()) // hook is this realm's address as Hyperlane carries it: the merkle tree hook id inside every checkpoint signed // for a message from here. hook = mustBytes32(chain.PackageAddress(pkgPath)) tree hl.Tree sent avl.Tree // nonceKey(nonce) → *sentMsg done avl.Tree // message id in hex → the block height it was delivered at verifiers avl.Tree // pkgpath → Verifier, every realm that registered active avl.Tree // domainKey(origin) → the pkgpath of the verifier GovDAO chose for that origin signerKeys []Key // the one signer set, in the order signatures name them signerRule Rule live bool // after the Launch decision executed launchedAt int64 // unix time it did yielded int64 // the block height the governor yielded at (Yield); 0 until then — every bridge realm reads it ledger decisions.Ledger ) // sentMsg is one dispatched message and the checkpoint it made. type sentMsg struct { raw string // the message as it went out root [32]byte // the tree's root right after its id went in, at index = its nonce height int64 } // Key is a signer's ed25519 public key. type Key [32]byte // Rule is how many signers must sign: Threshold of the set, or — when both groups are named (§11.13) — one key of // GroupA and one of GroupB. The verifier applies it; Satisfiable says whether a set can still meet it. type Rule struct { Threshold int GroupA []Key GroupB []Key } // TwoGroups is whether the rule is "one of A and one of B" rather than a threshold. func (r Rule) TwoGroups() bool { return len(r.GroupA) > 0 && len(r.GroupB) > 0 } // String says the rule in words. func (r Rule) String() string { if r.TwoGroups() { return "one of group A (" + keysHex(r.GroupA) + ") and one of group B (" + keysHex(r.GroupB) + ")" } return strconv.Itoa(r.Threshold) + " of the set" } func (r Rule) equal(o Rule) bool { return r.Threshold == o.Threshold && sameKeys(r.GroupA, o.GroupA) && sameKeys(r.GroupB, o.GroupB) } // sameAs is whether two rules are the same rule, the groups compared as sets. func (r Rule) sameAs(o Rule) bool { return r.Threshold == o.Threshold && sameSet(r.GroupA, o.GroupA) && sameSet(r.GroupB, o.GroupB) } // Verifier decides whether an incoming message is genuine (§5.1). A verifier is a realm; it registers once with // RegisterVerifier and GovDAO activates it by pkgpath. Both methods are non-crossing. An implementation is a type // declared in the registering realm: its methods then run under that realm's authority (borrow rule #1) and can // write nothing of this realm's. A type declared in /p/ with no object to anchor on (`type V int`) would run under // this realm's own authority instead — the no-anchor case — which RegisterVerifier cannot tell apart, so GovDAO // activates a verifier whose source it has read; what the mailbox hands a verifier is the recipient's bytes // (Verify) or a copy of its own keys (Satisfiable), never a value it keeps. type Verifier interface { Verify(origin uint32, metadata, message []byte) error Satisfiable(signers []Key) bool } func domainOf(chainID string) uint32 { if chainID == mainnetChainID { return DomainMainnet } return DomainTest } func solanaOf(chainID string) uint32 { if chainID == mainnetChainID { return SolanaMainnet } return SolanaLocal } func mustBytes32(a address) [32]byte { b, err := hl.AddressToBytes32(a) if err != nil { panic(err.Error()) } return b } func caller(cur realm) address { if !cur.IsCurrent() { panic("hlmailbox: that realm value is not live; pass your own cur") } return cur.Previous().Address() } func u32(n uint32) string { return strconv.FormatUint(uint64(n), 10) } func hx(b []byte) string { return hex.EncodeToString(b) } func keysHex(ks []Key) string { s := make([]string, len(ks)) for i, k := range ks { s[i] = hx(k[:]) } return strings.Join(s, ",") } // nonceKey sorts nonces by number. func nonceKey(n uint32) string { s := u32(n) return strings.Repeat("0", 10-len(s)) + s } func domainKey(d uint32) string { return nonceKey(d) } func init() { signerKeys = parseKeys(launchSigners) signerRule = Rule{Threshold: launchThreshold} active.Set(domainKey(launchOrigin), launchVerifier) initSeats() routes.Set(launchRouteGNOT, &routeRec{}) routes.Set(launchRouteBUBBLE, &routeRec{}) } // ------------------------------------------------------------- reading -- // LocalDomain is this chain's Hyperlane domain id. func LocalDomain() uint32 { return localDomain } // LaunchOrigin is the Solana domain the launch values name. func LaunchOrigin() uint32 { return launchOrigin } // Address is this realm's address. func Address() address { return chain.PackageAddress(pkgPath) } // Hook is this mailbox's merkle tree hook id in hex: the 32 bytes every checkpoint for a message from here names. func Hook() string { return hx(hook[:]) } // Count is how many messages went out: the next message's nonce. func Count() uint32 { return tree.Count } // Root is the merkle tree's root now, in hex. func Root() string { r := tree.Root() return hx(r[:]) } // Live is whether the Launch decision has executed: before it the routes refuse sends and credits. func Live() bool { return live } // LaunchedAt is the unix time the Launch decision executed, 0 before: the routes' meters start empty there. func LaunchedAt() int64 { return launchedAt } // Governor is the compiled governor (M1k): the address whose own call decides until it yields; "" when GovDAO // governs from the first block. Every other bridge realm compiles the same and checks it here at deploy. func Governor() address { return launchGovernor } // Yielded is the block height the governor yielded at (Yield), 0 until then: what every bridge realm reads before // it accepts a decision — the governor's own call before, GovDAO's proxy after (decisions.DAOGoverns). func Yielded() int64 { return yielded } // Message is the message dispatched with this nonce, in hex, and the tree's root right after it ("", "" if none). func Message(nonce uint32) (message, root string) { v := sent.Get(nonceKey(nonce)) if v == nil { return "", "" } s := v.(*sentMsg) return hx([]byte(s.raw)), hx(s.root[:]) } // Delivered is the block height a message (by id, in hex) was delivered at here; 0 if it never was. func Delivered(id string) int64 { v := done.Get(cleanID(id)) if v == nil { return 0 } return v.(int64) } func cleanID(id string) string { return strings.TrimPrefix(strings.ToLower(strings.TrimSpace(id)), "0x") } // Signers is the signer set and its rule: copies, so a verifier can read them and nobody can change them. func Signers() ([]Key, Rule) { ks := append([]Key(nil), signerKeys...) r := Rule{Threshold: signerRule.Threshold, GroupA: append([]Key(nil), signerRule.GroupA...), GroupB: append([]Key(nil), signerRule.GroupB...)} return ks, r } // SignerKeys is the signer set in hex, comma separated, in the order signatures name them. func SignerKeys() string { return keysHex(signerKeys) } // ActiveVerifier is the pkgpath of the verifier GovDAO chose for an origin ("" if none). func ActiveVerifier(origin uint32) string { v := active.Get(domainKey(origin)) if v == nil { return "" } return v.(string) } // VerifierRegistered is whether a realm at this pkgpath registered as a verifier. func VerifierRegistered(path string) bool { return verifiers.Has(path) } // --------------------------------------------------------------- keys -- func parseKey(s string) Key { p := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(s)), "0x") b, err := hex.DecodeString(p) if err != nil || len(b) != 32 { panic("hlmailbox: not a 32-byte ed25519 key in hex: " + p) } var k Key copy(k[:], b) return k } // parseKeys reads comma-separated keys in hex, refusing a key named twice. func parseKeys(s string) []Key { if strings.TrimSpace(s) == "" { return nil } parts := strings.Split(s, ",") if len(parts) > maxSigners { panic("hlmailbox: more than 255 keys") } ks := make([]Key, 0, len(parts)) for _, p := range parts { k := parseKey(p) if indexOf(ks, k) >= 0 { panic("hlmailbox: a key named twice: " + hx(k[:])) } ks = append(ks, k) } return ks } func indexOf(ks []Key, k Key) int { for i, x := range ks { if x == k { return i } } return -1 } // sameKeys is whether two lists hold the same keys in the same order. func sameKeys(a, b []Key) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } // subset is whether every key of a is in b. func subset(a, b []Key) bool { for _, k := range a { if indexOf(b, k) < 0 { return false } } return true } // sameSet is whether two lists hold the same keys, in any order (a list never names a key twice). func sameSet(a, b []Key) bool { return len(a) == len(b) && subset(a, b) && subset(b, a) } // ----------------------------------------------------------- verifiers -- // RegisterVerifier is how a verifier realm offers itself: called once from that realm (its init, usually), it is // kept under the caller's pkgpath, which GovDAO names to activate it. Only an /r/ realm may register, and once; // a sub-realm identity (`cur.Sub(…)`, a token a realm mints for free, whose pkgpath `host#subpath` passes the // prefix test) is refused, so every entry costs a package deployment and the listing (bounded; render.gno) // cannot be flooded. Nothing is ever unregistered. func RegisterVerifier(cur realm, v Verifier) { if !cur.IsCurrent() { panic("hlmailbox: that realm value is not live; pass your own cur") } path := cur.Previous().PkgPath() if !strings.HasPrefix(path, "gno.land/r/") { panic("hlmailbox: only a realm under gno.land/r/ registers a verifier, not " + path) } if cur.Previous().Subpath() != "" { panic("hlmailbox: a realm registers with its own cur, not a sub-identity (" + path + ")") } if v == nil { panic("hlmailbox: a nil verifier") } if verifiers.Has(path) { panic("hlmailbox: " + path + " registered already") } verifiers.Set(path, v) chain.Emit("VerifierRegistered", "pkgpath", path) } // satisfiable asks every active verifier whether the signer set ks can still meet the rule; an active verifier // that is not registered answers no. Each verifier is handed its own copy of ks: the builders store ks after // asking, and a verifier that runs under this realm's authority (the no-anchor case, see Verifier) must find // nothing of the mailbox's to write into. func satisfiable(ks []Key) bool { ok := true active.Iterate("", "", func(_ string, v any) bool { path := v.(string) r := verifiers.Get(path) if r == nil || !r.(Verifier).Satisfiable(append([]Key(nil), ks...)) { ok = false return true } return false }) return ok } // ---------------------------------------------------------------- out -- // Dispatch sends body to recipient on destination, from whoever calls it — a realm, usually — and returns the // message's id. The sender is the caller's address as Hyperlane carries one; the nonce is the message's index in // the merkle tree. The event is §13's: id, nonce, origin, destination, sender, recipient, hash. func Dispatch(cur realm, destination uint32, recipient [32]byte, body []byte) [32]byte { sender := mustBytes32(caller(cur)) // a successor a re-point dropped is a route no more (R35): Solana's token may still name it the router until // the next kind 6, so a message from it would mint against a lock no realm of the line holds if path := cur.Previous().PkgPath(); path != "" { if by := Dropped(path); by != "" { panic("hlmailbox: " + path + " is a route no more (its predecessor " + by + " re-pointed the migration to another successor): it sends nothing") } } m := hl.Message{ Version: hl.Version, Nonce: tree.Count, Origin: localDomain, Sender: sender, Destination: destination, Recipient: recipient, Body: body, } raw := m.Bytes() id := hl.ID(raw) tree.Insert(id) root := tree.Root() sent.Set(nonceKey(m.Nonce), &sentMsg{raw: string(raw), root: root, height: runtime.ChainHeight()}) emitDispatch(m, raw, id) return id } func emitDispatch(m hl.Message, raw []byte, id [32]byte) { h := sha256.Sum256(raw) chain.Emit("Dispatch", "id", hx(id[:]), "nonce", u32(m.Nonce), "origin", u32(m.Origin), "destination", u32(m.Destination), "sender", hx(m.Sender[:]), "recipient", hx(m.Recipient[:]), "hash", hx(h[:]), ) } // Reemit emits the Dispatch event of a message dispatched earlier again, in this block, by anyone: a message // dispatched before a validator-set change, or whose results leaf is bigger than the ISM accepts, is then delivered // from a fresh header. The id is unchanged, so the other side's processed-message record still refuses a second // delivery. func Reemit(cur realm, nonce uint32) { caller(cur) v := sent.Get(nonceKey(nonce)) if v == nil { panic("hlmailbox: no message with nonce " + u32(nonce)) } raw := []byte(v.(*sentMsg).raw) m, err := hl.ParseMessage(raw) if err != nil { panic(err.Error()) } emitDispatch(m, raw, hl.ID(raw)) } // ----------------------------------------------------------------- in -- // Process takes a message from another chain for the caller, who must be its recipient — or a route that // succeeds the recipient along the chain of migrations (AQ1: a burn addressed to a migrated route is the // successor's to credit, once its last page is in; the route decides when), a successor a re-point dropped from // that chain counted as a hop of its predecessor's (R35: the line's last realm takes what is addressed to it; the // dropped realm itself, a route no more, is refused). metadata is whatever the origin's active verifier needs // (the signers' checkpoint and signatures, for the ed25519 one). The message must be for this chain and never // delivered; it is marked delivered, then the verifier is asked, and a verifier's error is a panic, so the whole // transaction reverts with the mark and the message stays deliverable (§5.1). It is then handed back for the // recipient to act on. func Process(cur realm, metadata, message []byte) (origin uint32, sender [32]byte, body []byte) { who := caller(cur) callerPath := cur.Previous().PkgPath() if by := Dropped(callerPath); by != "" { panic("hlmailbox: " + callerPath + " is a route no more (its predecessor " + by + " re-pointed the migration to another successor): the line's last realm takes this message") } m, err := hl.ParseMessage(message) if err != nil { panic(err.Error()) } if m.Version != hl.Version { panic("hlmailbox: message version " + strconv.Itoa(int(m.Version)) + ", not " + strconv.Itoa(hl.Version)) } if m.Destination != localDomain { panic("hlmailbox: the message is for domain " + u32(m.Destination) + ", not this one (" + u32(localDomain) + ")") } if mustBytes32(who) != m.Recipient && !successorOf(callerPath, m.Recipient) { panic("hlmailbox: only the message's recipient, or a successor of it, can take it") } idb := hl.ID(message) id := hx(idb[:]) if done.Has(id) { panic("hlmailbox: already delivered") } path := active.Get(domainKey(m.Origin)) if path == nil { panic("hlmailbox: no verifier for domain " + u32(m.Origin)) } v := verifiers.Get(path.(string)) if v == nil { panic("hlmailbox: the active verifier " + path.(string) + " is not registered") } done.Set(id, runtime.ChainHeight()) if err := v.(Verifier).Verify(m.Origin, metadata, message); err != nil { panic(err.Error()) } chain.Emit("Process", "origin", u32(m.Origin), "nonce", u32(m.Nonce), "id", id, "sender", hx(m.Sender[:]), "recipient", hx(m.Recipient[:]), ) return m.Origin, m.Sender, m.Body } // MarkDone marks a message id delivered without a message: a route's credit-by-vote (§5.3) does it, so the id // can never be credited twice. Only a registered route may call it, and only for an id not done. func MarkDone(cur realm, id string) { mustBeRoute(cur) id = cleanID(id) if b, err := hex.DecodeString(id); err != nil || len(b) != 32 { panic("hlmailbox: not a 32-byte message id in hex: " + id) } if done.Has(id) { panic("hlmailbox: already delivered") } done.Set(id, runtime.ChainHeight()) }
  10. #10render.gno
  11. #11package hlmailbox import ( "strconv" "strings" "gno.land/p/nt/avl/v0" "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/decisions/v0" hl "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hyperlane/v0" ) const ( pageMax = 20 // messages a "msgs/" page holds verifiersMax = 20 // registered verifier paths (and announced successors) the page and "json" name, and a "verifiers/" ("successors/") page holds ) // Render: // // "" the mailbox's page: the launch values live or waiting, verifiers, signers, seats, flags // "decisions" the decisions page: the last 100 pending and executed per kind // "json" domain, hook, count, root, live, signers, verifiers (the first 20 registered paths and the // count), seats, flags, routes (each with its predecessor and successor, and the height and // time of the kind 6 that enrolled it on Solana: enrolled, enrolledAt), successors (the // first 20 announced paths, the count, and successorRoutes: the route each announced for), // for the agents // "decisions.json" every decision, as a JSON array // "msg/<nonce>" one dispatched message: {"nonce","id","root","height","message"}; "null" if none // "msgs/<from>" up to 20 messages from nonce <from> on, as a JSON array // "verifiers/<from>" up to 20 registered verifier paths from offset <from> on, in path order, as a JSON array // "successors/<from>" up to 20 announced successor paths from offset <from> on, in path order, as a JSON array // "delivered/<id>" the block height a message id (hex) was delivered at here, 0 if never func Render(path string) string { switch { case path == "": return page() case path == "decisions": return "# Decisions\n\n" + ledger.Page() case path == "json": return summaryJSON() case path == "decisions.json": return ledger.JSON() case strings.HasPrefix(path, "msg/"): n, ok := parseNonce(path[len("msg/"):]) if !ok { return "null" } return msgJSON(n) case strings.HasPrefix(path, "msgs/"): from, ok := parseNonce(path[len("msgs/"):]) if !ok { return "[]" } var b strings.Builder b.WriteString("[") for i := uint32(0); i < pageMax && uint64(from)+uint64(i) < uint64(tree.Count); i++ { if i > 0 { b.WriteString(",") } b.WriteString(msgJSON(from + i)) } b.WriteString("]") return b.String() case strings.HasPrefix(path, "verifiers/"): from, ok := parseNonce(path[len("verifiers/"):]) if !ok { return "[]" } return pathsJSON(&verifiers, int(from)) case strings.HasPrefix(path, "successors/"): from, ok := parseNonce(path[len("successors/"):]) if !ok { return "[]" } return pathsJSON(&successors, int(from)) case strings.HasPrefix(path, "delivered/"): return strconv.FormatInt(Delivered(path[len("delivered/"):]), 10) } return "unknown path; see the mailbox's page" } func parseNonce(s string) (uint32, bool) { n, err := strconv.ParseUint(s, 10, 32) return uint32(n), err == nil } func q(s string) string { return strconv.Quote(s) } func msgJSON(n uint32) string { v := sent.Get(nonceKey(n)) if v == nil { return "null" } s := v.(*sentMsg) id := hl.ID([]byte(s.raw)) return "{\"nonce\":" + u32(n) + ",\"id\":" + q(hx(id[:])) + ",\"root\":" + q(hx(s.root[:])) + ",\"height\":" + strconv.FormatInt(s.height, 10) + ",\"message\":" + q(hx([]byte(s.raw))) + "}" } func keysJSON(ks []Key) string { parts := make([]string, len(ks)) for i, k := range ks { parts[i] = q(hx(k[:])) } return "[" + strings.Join(parts, ",") + "]" } // pathsJSON is up to verifiersMax keys of a tree of pkgpaths (the registered verifiers, the announced successors) // from offset from on, in path order: the listing is bounded whatever the count, since nothing is ever removed. func pathsJSON(t *avl.Tree, from int) string { var b strings.Builder b.WriteString("[") first := true t.IterateByOffset(from, verifiersMax, func(k string, _ any) bool { if !first { b.WriteString(",") } first = false b.WriteString(q(k)) return false }) b.WriteString("]") return b.String() } // pathsLine is a tree of pkgpaths on the page: the first verifiersMax in path order and, past them, where the rest // are (page is the Render path that pages them); "none" for an empty tree. func pathsLine(t *avl.Tree, page string) string { n := t.Size() if n == 0 { return "none" } var b strings.Builder i := 0 t.IterateByOffset(0, verifiersMax, func(k string, _ any) bool { if i > 0 { b.WriteString(", ") } i++ b.WriteString("`" + k + "`") return false }) if n > verifiersMax { m := strconv.Itoa(verifiersMax) b.WriteString(", … (the first " + m + "; the rest at [" + page + "/" + m + "](" + pkgPathLink() + ":" + page + "/" + m + "))") } return b.String() } func summaryJSON() string { var b strings.Builder b.WriteString("{\"domain\":" + u32(localDomain) + ",\"hook\":" + q(Hook()) + ",\"address\":" + q(string(Address())) + ",\"count\":" + u32(tree.Count) + ",\"root\":" + q(Root()) + ",\"delivered\":" + strconv.Itoa(done.Size()) + ",\"live\":" + strconv.FormatBool(live) + ",\"launchedAt\":" + strconv.FormatInt(launchedAt, 10) + "," + decisions.GovernorJSON(launchGovernor, yielded) + ",\"launchOrigin\":" + u32(launchOrigin) + ",\"clocks\":" + decisions.ClocksJSON() + ",\"decisions\":" + strconv.FormatInt(ledger.Last(), 10) + ",\"signers\":{\"keys\":" + keysJSON(signerKeys) + ",\"threshold\":" + strconv.Itoa(signerRule.Threshold) + ",\"groupA\":" + keysJSON(signerRule.GroupA) + ",\"groupB\":" + keysJSON(signerRule.GroupB) + "}" + ",\"verifiers\":{") first := true active.Iterate("", "", func(k string, v any) bool { origin, _ := strconv.ParseUint(k, 10, 32) if !first { b.WriteString(",") } first = false b.WriteString(q(strconv.FormatUint(origin, 10)) + ":{\"pkgpath\":" + q(v.(string)) + ",\"registered\":" + strconv.FormatBool(verifiers.Has(v.(string))) + "}") return false }) b.WriteString("},\"registered\":" + pathsJSON(&verifiers, 0) + ",\"registeredCount\":" + strconv.Itoa(verifiers.Size()) + ",\"seats\":[") for i := 1; i <= seatCount; i++ { if i > 1 { b.WriteString(",") } b.WriteString(q(string(seats[i]))) } b.WriteString("],\"routes\":{") first = true routes.Iterate("", "", func(k string, v any) bool { r := v.(*routeRec) if !first { b.WriteString(",") } first = false b.WriteString(q(k) + ":{\"epoch\":" + strconv.FormatInt(r.epoch, 10) + ",\"epochAt\":" + strconv.FormatInt(r.epochAt, 10) + ",\"sends\":" + strconv.FormatBool(Paused(k, FnSends)) + ",\"credits\":" + strconv.FormatBool(Paused(k, FnCredits)) + ",\"payouts\":" + strconv.FormatBool(Paused(k, FnPayouts)) + ",\"flags\":[") for i, f := range Flags(k) { if i > 0 { b.WriteString(",") } b.WriteString(q(f)) } b.WriteString("],\"predecessor\":" + q(r.predecessor) + ",\"successor\":" + q(r.successor) + ",\"enrolled\":" + strconv.FormatInt(r.enrolled, 10) + ",\"enrolledAt\":" + strconv.FormatInt(r.enrolledAt, 10) + ",\"pulled\":" + strconv.FormatBool(r.pulled) + ",\"dropped\":" + quotedJSON(r.dropped) + "}") return false }) b.WriteString("},\"droppedRoutes\":" + droppedRoutesJSON() + ",\"successors\":" + pathsJSON(&successors, 0) + ",\"successorCount\":" + strconv.Itoa(successors.Size()) + ",\"successorRoutes\":" + successorRoutesJSON() + "}") return b.String() } // quotedJSON is a list of strings as a JSON array. func quotedJSON(ss []string) string { parts := make([]string, len(ss)) for i, s := range ss { parts[i] = q(s) } return "[" + strings.Join(parts, ",") + "]" } // droppedRoutesJSON is every dropped successor (R35) with the predecessor whose re-point dropped it, as an object // of pkgpath → pkgpath: the tombstones, bounded by the migrations a bridge sees in its life. func droppedRoutesJSON() string { var b strings.Builder b.WriteString("{") first := true droppedRoutes.Iterate("", "", func(k string, v any) bool { if !first { b.WriteString(",") } first = false b.WriteString(q(k) + ":" + q(v.(string))) return false }) b.WriteString("}") return b.String() } // droppedLine is the dropped successors on the page, each with the predecessor that re-pointed away from it; "" // when none. func droppedLine() string { if droppedRoutes.Size() == 0 { return "" } var parts []string droppedRoutes.Iterate("", "", func(k string, v any) bool { parts = append(parts, "`"+k+"` (by `"+v.(string)+"`)") return false }) return "- dropped (routes no more, re-pointed away): " + strings.Join(parts, ", ") + "\n" } // successorRoutesJSON is the first verifiersMax announced successors with the route each announced for (AQ5), as // an object of pkgpath → "GNOT" or "BUBBLE". func successorRoutesJSON() string { var b strings.Builder b.WriteString("{") first := true successors.IterateByOffset(0, verifiersMax, func(k string, v any) bool { if !first { b.WriteString(",") } first = false b.WriteString(q(k) + ":" + q(v.(string))) return false }) b.WriteString("}") return b.String() } // successorsLine is the announced successors on the page, each with the route it announced for (AQ5): the first // verifiersMax in path order and, past them, where the rest are; "none" for an empty tree. func successorsLine() string { n := successors.Size() if n == 0 { return "none" } var b strings.Builder i := 0 successors.IterateByOffset(0, verifiersMax, func(k string, v any) bool { if i > 0 { b.WriteString(", ") } i++ b.WriteString("`" + k + "` (" + v.(string) + ")") return false }) if n > verifiersMax { m := strconv.Itoa(verifiersMax) b.WriteString(", … (the first " + m + "; the rest at [successors/" + m + "](" + pkgPathLink() + ":successors/" + m + "))") } return b.String() } func page() string { var b strings.Builder b.WriteString("# The bridge's mailbox\n\n") b.WriteString("gno.land's end of the GNOT and BUBBLE bridge to Solana: messages out to Solana (the Dispatch event the light client proves), and in from it under the active verifier. Every setting here moves only by a decision made with this realm's builders — the governor's own until it yields, a GovDAO proposal after; the guardians and watch keys can only pause.\n\n") if live { b.WriteString("- **live** since " + strconv.FormatInt(launchedAt, 10) + " (the Launch decision executed)\n") } else { b.WriteString("- **not launched**: the values below are the compiled launch values, waiting for the Launch decision; the routes refuse sends and credits. A passed Launch executes within 14 days of its proposal, like every decision; one nobody executed by then lapses and is proposed again\n") } b.WriteString("- " + decisions.GovernorLine(launchGovernor, yielded) + "\n") b.WriteString("- a proposal made with a builder (of this realm, a route, r/hlfees or r/hlgov) is executed with r/gov/dao's `ExecuteOrRejectProposal`: the one error a closure returns — the 14-day deadline — then denies it for good; under `ExecuteProposal` that error reverts the transaction instead and the proposal stands, every retry failing the same way. Anything else a closure refuses is a panic, which leaves the proposal for a retry under either call\n") b.WriteString("- domain: " + u32(localDomain) + "; Solana: " + u32(launchOrigin) + "\n") b.WriteString("- " + decisions.Clocks() + "\n") b.WriteString("- merkle tree hook: `" + Hook() + "`\n") b.WriteString("- messages out: " + u32(tree.Count) + ", root `" + Root() + "`; in: " + strconv.Itoa(done.Size()) + "\n") b.WriteString("- decisions: " + strconv.FormatInt(ledger.Last(), 10) + " ([page](" + pkgPathLink() + ":decisions))\n") b.WriteString("\n## Verifiers\n\n") active.Iterate("", "", func(k string, v any) bool { origin, _ := strconv.ParseUint(k, 10, 32) note := "" if !verifiers.Has(v.(string)) { note = " (**not registered**: nothing from this origin is taken until it is)" } b.WriteString("- from " + strconv.FormatUint(origin, 10) + ": `" + v.(string) + "`" + note + "\n") return false }) b.WriteString("- registered (" + strconv.Itoa(verifiers.Size()) + "): " + pathsLine(&verifiers, "verifiers")) b.WriteString("\n\n## Signers\n\n- rule: " + signerRule.String() + "\n") for i, k := range signerKeys { b.WriteString("- " + strconv.Itoa(i) + ": `" + hx(k[:]) + "`\n") } b.WriteString("\n## Seats\n\n") for i := 1; i <= seatCount; i++ { kind := "guardian" if !IsGuardianSeat(i) { kind = "watch key" } a := string(seats[i]) if a == "" { a = "(empty)" } b.WriteString("- seat " + strconv.Itoa(i) + " (" + kind + "): " + a + "\n") } b.WriteString("\n## Routes and flags\n\n") routes.Iterate("", "", func(k string, v any) bool { r := v.(*routeRec) b.WriteString("- `" + k + "`: payouts epoch " + strconv.FormatInt(r.epoch, 10)) if r.predecessor != "" { b.WriteString(", successor of `" + r.predecessor + "`") } if r.pulled { b.WriteString(", page 0 pulled") } if r.successor != "" { b.WriteString(", migrated to `" + r.successor + "`") } if len(r.dropped) > 0 { b.WriteString(", dropped `" + strings.Join(r.dropped, "`, `") + "`") } if r.enrolled != 0 { b.WriteString(", enrolled on Solana by kind 6 at height " + strconv.FormatInt(r.enrolled, 10) + " (" + strconv.FormatInt(r.enrolledAt, 10) + ")") } fs := Flags(k) if len(fs) == 0 { b.WriteString(", no flag\n") } else { b.WriteString("\n") for _, f := range fs { b.WriteString(" - **paused** " + f + "\n") } } return false }) b.WriteString(droppedLine()) b.WriteString("- successors announced (" + strconv.Itoa(successors.Size()) + "): " + successorsLine() + " (a route's migrate names only one of them, announced for its own route)\n") if tree.Count > 0 { b.WriteString("\n## Latest out\n\n") sent.ReverseIterateByOffset(0, 10, func(k string, v any) bool { s := v.(*sentMsg) m, _ := hl.ParseMessage([]byte(s.raw)) id := hl.ID([]byte(s.raw)) b.WriteString("- #" + u32(m.Nonce) + " to " + u32(m.Destination) + " at block " + strconv.FormatInt(s.height, 10) + ": `" + hx(id[:]) + "`\n") return false }) } return b.String() } func pkgPathLink() string { return strings.TrimPrefix(pkgPath, "gno.land") }
  12. #12/gno.MemPackageType
  13. #13 MPUserAll

Result log

msg:0,success:true,log:,events:[]