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

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

hlgov.gno

44.98 Kb · 978 lines
  1// Package hlgov carries GovDAO's decisions to Solana (bridge/PLAN.md §5.3, §6.5, §13): a proposal made with one of
  2// the builders below, when it executes, dispatches a message through r/hlmailbox to the admin program — the
  3// decision's kind, its parameters and its sequence number (this realm's own counter, taken at execution) — and the
  4// admin program records it by that number and executes it after its clock. Anyone may re-dispatch an executed
  5// decision (the same number and parameters) until the admin program has recorded it. The builders check the
  6// parameters with the admin program's own rules, so a proposal that would fail on Solana is refused when it is made.
  7// Kind 4 is reserved and its builder refuses every call (§13); a cancel's body starts with an 8-byte salt drawn in
  8// the executing transaction, so two cancels of one list differ; kind 12 names a token's mint limits on the pausable
  9// ISM (the per-message maximum and the day cap, in base units), one of the 7-day group.
 10//
 11// Two decisions change this realm alone: `set decision recipient` (the program id decisions go to, a 7-day change,
 12// how a successor admin program ever receives one) and `sync sequence` (at once: the counter is raised — never
 13// lowered — to the number the admin program expects next, when a foreign decision has taken one, §6.5). The counter
 14// starts at a compiled number (launchSequence: 0 for a chain's first r/hlgov, the predecessor's last number for a
 15// successor), so no number the admin program recorded is ever reused. Before the Launch decision every builder
 16// but the cancel of a queued change here is refused. Who decides is the governor's (M1k): the compiled
 17// launchGovernor through Govern until it yields on r/hlmailbox, GovDAO's proposals after. Every launch value is
 18// compiled in below.
 19package hlgov
 20
 21import (
 22	"chain"
 23	"chain/runtime"
 24	"crypto/sha256"
 25	"encoding/binary"
 26	"encoding/hex"
 27	"strconv"
 28	"strings"
 29	"time"
 30
 31	"gno.land/p/nt/avl/v0"
 32	"gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/decisions/v0"
 33	hl "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hyperlane/v0"
 34	"gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hlbubble"
 35	"gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hlgnot"
 36	"gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hlmailbox"
 37	"gno.land/r/gov/dao"
 38)
 39
 40const (
 41	namespace = "gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/"
 42	pkgPath   = namespace + "hlgov" // must match gnomod.toml
 43
 44	// The kinds (§13).
 45	KindCancel       = uint8(1)
 46	KindUnpause      = uint8(2)
 47	KindTokenISM     = uint8(3)
 48	KindReserved     = uint8(4) // reserved (§13): the client's state layout is fixed for the deployment's life; refused
 49	KindGuardians    = uint8(5)
 50	KindRouter      = uint8(6)
 51	KindProtocolFee = uint8(7)
 52	KindUpgrade     = uint8(8)
 53	KindSuccessor   = uint8(9)
 54	KindMintLimits  = uint8(12) // set mint limits (§13): a token's per-message maximum and day cap on the pausable ISM; the 7-day group
 55
 56	maxCancel      = 16         // numbers one cancel names
 57	maxProtocolFee = 50_000_000 // lamports: the mailbox's max_protocol_fee, 0.05 SOL, fixed at its init (§5.5)
 58	// MaxDecisionBody bounds a decision's body (kind ‖ number ‖ params) at what Solana can ever deliver (the
 59	// assumptions audit's F18, M1g part 2): a legacy delivery of a decision carries at most 538 bytes of body — 529
 60	// of parameters, what 14 account keys and 127 bytes of fixed instruction data leave of the 1,232-byte packet —
 61	// and Hyperlane's mainnet mailbox dies on its own 32 KB heap past about 2,250 parameter bytes anyway (M2-NOTES
 62	// M2f), so a body no one can deliver would stick this realm's sequence with its number untaken, in a form no
 63	// program of ours cures. Every builder's body is far below (a cancel at most 163 bytes with its salt, kind 5 at
 64	// most 4 entries); the bound stands where this realm dispatches, checked when a proposal is made and in its
 65	// closure.
 66	MaxDecisionBody = 512
 67	syncReach      = uint64(1) << 32 // a sync sequence raises the counter by at most this (the realm's own bound, §13; kind 10 is reserved since v35)
 68	seatCount      = 6
 69	guardianSeats  = 4
 70	maxSeatChanges = 4 // kind 5: two guardian seats plus either watch slot
 71
 72	// The launch values (§5.3, §6.5): the admin program (the first decision recipient), the pausable ISM (the only
 73	// id kind 3 may name), the light client (the pausable ISM's target, whose state layout is fixed for the
 74	// deployment's life — kind 4 is reserved — shown on the page for a member's check), and the four guardians' and
 75	// two watch keys' Solana pubkeys as the admin program's init data carries them. PLACEHOLDERS until the Solana
 76	// programs are deployed and the people have keys; bridge/local/e2e.sh swaps the program ids for its run's.
 77	launchRecipient   = "38885bd746ab9dc9b6175331a61842d0ad8e0c4c2532f72e286ef7dac82b0358" // the admin program 4ogRnw9iPGa4dY8qqCs1aBWxHQvKSzHh5ttz53sLFTM9
 78	launchPausableISM = "564eab462a321148f88d548996d0fd5e84c66ab630f0f4da63331592d7dce221" // the pausable ISM 6oudxto59ktYrpvqEVXTb5Tuo7vzBpkgnsPWoPXVF5Fv
 79	launchClient      = "1a572d156787a62750999f343e5af6f7394f91c11f75a2949e9aefd8914e07ad" // the light client 2mphYtLZ7HnCfrFhGH2c4Do9C9UoysevkmkDb8UkbAu2
 80	launchSolSeat1    = "05a1af200cfddc3b3ace76353505098855c9994913f7282f2e45ce6e473dcd6e" // Nz2u3N3RAMwq9pjw1Vv7ktN7nX4rNe4Uw5rdL9o9c3o
 81	launchSolSeat2    = "028b184c1fe43ac501409db4eb45a47bc2cd280226cca57345b802d27175e5b4" // AvqEe2hdx3HH87NU9CR5GSarCD1sR6PQ2H5AjwSnC4b
 82	launchSolSeat3    = "95b3b44e5adc9a0e849285e59cf6e7b0dab53e3a8772f680990f73a6111f494c" // B5NgM4oD1vYKTEGSe33RaSsHbyGzxKvoaycGqhjbyEV1
 83	launchSolSeat4    = "05ec99dd32dbee307eb064892f68581b9da9ee928df5a00d7f491f0f1b4fc3d2" // Q8HoML8efSufEhvYsSffmHAGJ6YnYb2QwyMVwQYt9Ds
 84	launchSolSeat5    = "195a00fb58ce0d7f762cc2b3d72017100724768fa2ae828c055d57530b34077a" // 2hxnxGiPf6cuuYXjThehyDnkXpUKjQai8fp14zkcgzjB
 85	launchSolSeat6    = "24551f378d8bbdc915fd4497aa9bf73a5eb5f99447755eb9f7c751aed3d558e2" // 3SpxeQf1JTfErvkpd3Kq33Q72H4yhLynKPVECdj9ckmf
 86
 87	// launchSequence is the number of the last decision this realm's predecessor dispatched — 0 for a chain's
 88	// first r/hlgov. A successor r/hlgov for a new chain is deployed with its predecessor's last number here
 89	// (§5.3), so its counter starts where the admin program's stands and never restarts below a recorded number.
 90	// Written by init; the page shows it beside the counter.
 91	launchSequence = uint64(0)
 92)
 93
 94// launchGovernor is the governor (M1k): the mailbox's, compiled here too so a member reads it in this realm's
 95// served source; init refuses a deploy with another. bridge/local/e2e.sh swaps it for gnodev's test1.
 96const launchGovernor = address("g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6")
 97
 98var (
 99	destination = hlmailbox.LaunchOrigin()
100	recipient   = mustKey(launchRecipient)
101	former      [][32]byte               // every decision recipient a swap replaced (R29): retired admin programs, which kind 9 and a swap refuse for good
102	client      = mustKey(launchClient)   // the light client: shown on the page; no kind names it (kind 4 is reserved)
103	solSeats    [seatCount + 1][32]byte // Solana's seats as this realm last set them: a mirror kind 5 warns against, shown on the page
104
105	last uint64   // the number of the last decision dispatched; the next is last + 1
106	sent avl.Tree // seqKey(number) → *decision
107
108	ledger decisions.Ledger
109)
110
111// decision is one dispatched decision: its body (kind ‖ number ‖ params) and its dispatches; a kind 6 for a route
112// of this chain also keeps the route it enrolled and the height it noted with the mailbox (R37: a cancel of it
113// clears that note while it is the latest).
114type decision struct {
115	kind   uint8
116	body   []byte
117	at     int64  // first dispatched
118	id     string // the latest message id
119	count  int    // dispatches so far
120	text   string
121	route  string // a kind 6 for this chain's domain: the route realm it enrolled; "" otherwise
122	height int64  // … and the block height NoteEnrolled recorded for it
123}
124
125type (
126	recipientParams struct{ to [32]byte }
127	sequenceParams  struct{ n uint64 }
128	cancelParams    struct{ id int64 }
129	kindParams      struct {
130		kind   uint8
131		params []byte
132		seats  []int
133		keys   [][32]byte
134	}
135)
136
137func init() {
138	// a deploy whose compiled governor is not the mailbox's is refused (M1k): the bridge's realms are governed by
139	// one key until it yields, or by GovDAO from the first block, never a mix
140	if launchGovernor != hlmailbox.Governor() {
141		panic("hlgov: the compiled governor " + string(launchGovernor) + " is not the mailbox's " + string(hlmailbox.Governor()))
142	}
143	solSeats[1], solSeats[2], solSeats[3] = mustKey(launchSolSeat1), mustKey(launchSolSeat2), mustKey(launchSolSeat3)
144	solSeats[4], solSeats[5], solSeats[6] = mustKey(launchSolSeat4), mustKey(launchSolSeat5), mustKey(launchSolSeat6)
145	last = launchSequence
146}
147
148func mustKey(s string) [32]byte {
149	k, err := hl.ParseKey32(s)
150	if err != nil {
151		panic(err.Error())
152	}
153	return k
154}
155
156func now() int64 { return time.Now().Unix() }
157
158func itoa(n int64) string   { return strconv.FormatInt(n, 10) }
159func u64s(n uint64) string  { return strconv.FormatUint(n, 10) }
160func hx(b []byte) string    { return hex.EncodeToString(b) }
161func seqKey(n uint64) string { return strings.Repeat("0", 20-len(u64s(n))) + u64s(n) }
162
163func caller(cur realm) address {
164	if !cur.IsCurrent() {
165		panic("hlgov: that realm value is not live; pass your own cur")
166	}
167	return cur.Previous().Address()
168}
169
170func mustBeLive() {
171	if !hlmailbox.Live() {
172		panic("hlgov: not launched: before the Launch decision only the cancel of a queued change is proposed")
173	}
174}
175
176// Request is what a builder returns (M1k): the proposal GovDAO's proxy executes after the yield (Proposal), and
177// the same decision the governor runs directly before it (Govern); see hlgnot's.
178type Request struct {
179	proposal dao.ProposalRequest
180	text     string
181	decide   func(cur realm) error
182}
183
184// Proposal is the request as a GovDAO proposal: `dao.MustCreateProposal(cross(cur), req.Proposal())`.
185func (r Request) Proposal() dao.ProposalRequest { return r.proposal }
186
187// Description is the proposal's: the parameters as the builder wrote them.
188func (r Request) Description() string { return r.text }
189
190// who names a previous realm in a refusal: its pkgpath, or a user's address.
191func who(path string, a address) string {
192	if path != "" {
193		return path
194	}
195	return string(a)
196}
197
198// mustBeGovernor is a decision's identity check (M1k), read from the closure's own live cur: the governor's own
199// call until it yields on r/hlmailbox, gno.land/r/gov/dao's proxy exactly after (or from the first block, when no
200// governor was compiled); see hlgnot's.
201func mustBeGovernor(cur realm) {
202	prev := cur.Previous()
203	if decisions.DAOGoverns(launchGovernor, hlmailbox.Yielded()) {
204		if p := prev.PkgPath(); p != decisions.DAOPath {
205			panic("hlgov: only " + decisions.DAOPath + " executes a decision, not " + who(prev.PkgPath(), prev.Address()))
206		}
207		return
208	}
209	if !prev.IsUser() || prev.Address() != launchGovernor {
210		panic("hlgov: 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)")
211	}
212}
213
214// Govern is the governor's way in before the yield (M1k): the same closure GovDAO's proxy runs after it, called
215// non-crossing in this frame so it reads the governor from cur.Previous() — and dispatches through the mailbox
216// under this realm's own authority, as the proxy's execution does; refused for anyone else and for everyone once
217// the governor yielded on r/hlmailbox (or when none was compiled); see hlgnot's.
218func Govern(cur realm, r Request) {
219	if !cur.IsCurrent() {
220		panic("hlgov: that realm value is not live; pass your own cur")
221	}
222	if r.decide == nil {
223		panic("hlgov: not a builder's request")
224	}
225	if yielded := hlmailbox.Yielded(); decisions.DAOGoverns(launchGovernor, yielded) {
226		if launchGovernor == "" {
227			panic("hlgov: no governor was compiled, " + decisions.DAOPath + " governs from the first block: a decision is a GovDAO proposal, never Govern")
228		}
229		panic("hlgov: the governor yielded at height " + itoa(yielded) + ", " + decisions.DAOPath + " governs since: a decision is a GovDAO proposal, never Govern")
230	}
231	mustBeGovernor(cur)
232	if err := r.decide(cur); err != nil {
233		panic("hlgov: " + err.Error())
234	}
235}
236
237// ------------------------------------------------------------- reading --
238
239// Address is this realm's address: the sender the admin program accepts.
240func Address() address { return chain.PackageAddress(pkgPath) }
241
242// Recipient is the program decisions are addressed to, in hex.
243func Recipient() string { return hx(recipient[:]) }
244
245// Former lists every decision recipient a swap replaced, in hex, oldest first (R29): retired admin programs.
246func Former() []string {
247	out := make([]string, len(former))
248	for i, k := range former {
249		out[i] = hx(k[:])
250	}
251	return out
252}
253
254func isFormer(k [32]byte) bool {
255	for _, f := range former {
256		if f == k {
257			return true
258		}
259	}
260	return false
261}
262
263// mustNotBeFormer refuses a retired admin program as a kind 9's successor or as the decision recipient (R29): a
264// program whose authorities went to a successor answers Retired to every decision, and a kind 9 naming it dies on
265// its successor_init (NotInherited), so the decision would pend until a cancel — a wasted vote and a standing line
266// on hlwatch. Checked when a proposal is made, in its closure, and when a queued swap would apply.
267func mustNotBeFormer(k [32]byte, what string) {
268	if isFormer(k) {
269		panic("hlgov: never a former " + what + " (" + hx(k[:]) + "): it is retired, every authority handed to a successor by kind 9 — a kind 9 to it would die on its successor_init (NotInherited) and a swap to it would send every decision to a program that answers Retired")
270	}
271}
272
273// Destination is the Solana domain decisions go to.
274func Destination() uint32 { return destination }
275
276// Last is the number of the last decision dispatched; Next the number the next one takes; Start the compiled
277// number the counter began at (the predecessor's last number; 0 for a chain's first r/hlgov).
278func Last() uint64  { return last }
279func Next() uint64  { return last + 1 }
280func Start() uint64 { return launchSequence }
281
282// SolSeat is the Solana pubkey this realm last set for a seat (1–4 guardians, 5–6 watch keys), in hex.
283func SolSeat(n int) string {
284	if n < 1 || n > seatCount {
285		return ""
286	}
287	return hx(solSeats[n][:])
288}
289
290// Body is a dispatched decision's body in hex, by number ("" if none).
291func Body(n uint64) string {
292	v := sent.Get(seqKey(n))
293	if v == nil {
294		return ""
295	}
296	return hx(v.(*decision).body)
297}
298
299// -------------------------------------------------------------- encoding --
300
301func be32(n uint32) []byte { return binary.BigEndian.AppendUint32(nil, n) }
302func be64(n uint64) []byte { return binary.BigEndian.AppendUint64(nil, n) }
303
304// body is kind u8 ‖ seq u64 BE ‖ params (§13).
305func body(kind uint8, n uint64, params []byte) []byte {
306	b := make([]byte, 0, 9+len(params))
307	b = append(b, kind)
308	b = append(b, be64(n)...)
309	return append(b, params...)
310}
311
312// mustFit refuses a body above MaxDecisionBody: the one check every dispatch makes, so no builder can author a
313// decision the mailbox cannot deliver.
314func mustFit(raw []byte) {
315	if len(raw) > MaxDecisionBody {
316		panic("hlgov: the decision body is " + strconv.Itoa(len(raw)) + " bytes, above the " + strconv.Itoa(MaxDecisionBody) + " the mailbox delivers (MaxDecisionBody): it would stick the sequence with its number untaken")
317	}
318}
319
320// dispatch sends a decision with the next number and records it; a body above MaxDecisionBody is refused first.
321func dispatch(cur realm, kind uint8, params []byte, text string, t int64) uint64 {
322	n := last + 1
323	raw := body(kind, n, params)
324	mustFit(raw)
325	id := hlmailbox.Dispatch(cross(cur), destination, recipient, raw)
326	last = n
327	sent.Set(seqKey(n), &decision{kind: kind, body: raw, at: t, id: hx(id[:]), count: 1, text: text})
328	chain.Emit("DecisionSent", "number", u64s(n), "kind", strconv.Itoa(int(kind)), "id", hx(id[:]))
329	return n
330}
331
332// Redispatch sends an executed decision again — the same number and parameters, a new message — by anyone, until
333// the admin program has recorded it (a copy it has recorded is accepted and ignored there).
334func Redispatch(cur realm, n uint64) {
335	caller(cur)
336	v := sent.Get(seqKey(n))
337	if v == nil {
338		panic("hlgov: no decision " + u64s(n))
339	}
340	d := v.(*decision)
341	id := hlmailbox.Dispatch(cross(cur), destination, recipient, d.body)
342	d.id = hx(id[:])
343	d.count++
344	chain.Emit("DecisionSent", "number", u64s(n), "kind", strconv.Itoa(int(d.kind)), "id", hx(id[:]))
345}
346
347// ------------------------------------------------------------- builders --
348
349func propose(cur realm, kind, text string, apply func(cur realm, made, t int64)) Request {
350	if !cur.IsCurrent() {
351		panic("hlgov: that realm value is not live; pass your own cur")
352	}
353	made := now()
354	cb := func(cur realm) error {
355		mustBeGovernor(cur)
356		t := now()
357		if err := decisions.Expired(made, t); err != nil {
358			return err
359		}
360		apply(cur, made, t)
361		return nil
362	}
363	return Request{proposal: dao.NewProposalRequest("hlgov: "+kind, text, dao.NewSimpleExecutor(0, cur, cb, text)), text: text, decide: cb}
364}
365
366// proposeKind is a Solana decision's builder: at execution it dispatches the message and records the decision as
367// applied; the parameters were checked at creation and are checked again then, the body's size with them.
368func proposeKind(cur realm, kind uint8, name, text string, params []byte, check func()) Request {
369	mustBeLive()
370	mustFit(body(kind, 0, params))
371	check()
372	return propose(cur, name, text, func(cur realm, made, t int64) {
373		mustBeLive()
374		check()
375		n := dispatch(cur, kind, params, text, t)
376		d := ledger.Record(name, text, kindParams{kind: kind, params: params}, made, t, t)
377		ledger.Apply(d, t, "dispatched as decision "+u64s(n))
378	})
379}
380
381func parseNumber(p string) uint64 {
382	n, err := strconv.ParseUint(strings.TrimSpace(p), 10, 64)
383	if err != nil || n == 0 {
384		panic("hlgov: not a decision number: " + p)
385	}
386	return n
387}
388
389// salt is the 8 bytes a cancel body starts with (§13: random, ignored by the program,
390// so no body is predictable and two cancels of one list differ). gno.land gives a realm no block
391// hash, so it is sha256 over what the block and the decision give — the chain id, the height, the block time, the
392// mailbox's nonce, the decision number and the ledger's next id — drawn in the executing transaction; the decision
393// number alone makes two cancels of one list differ, the rest make it as unpredictable as the block it lands in.
394func salt(t int64) []byte {
395	b := []byte(runtime.ChainID())
396	b = append(b, be64(uint64(runtime.ChainHeight()))...)
397	b = append(b, be64(uint64(t))...)
398	b = append(b, be32(hlmailbox.Count())...)
399	b = append(b, be64(last+1)...)
400	b = append(b, be64(uint64(ledger.Last()+1))...)
401	h := sha256.Sum256(b)
402	return h[:8]
403}
404
405// NewCancelRequest is kind 1: cancel pending decisions of the mailbox channel on Solana (channel 0, the only one
406// this realm writes), by a list of numbers (comma separated, at most 16: form 0) or by a range "from-to",
407// inclusive (form 1); each number one this realm dispatched. It executes at once there. The body is
408// `salt u64 ‖ form u8 ‖ …` (§13), the salt drawn when the proposal executes, so the description names the numbers
409// and the ledger's note and the decision's page carry the salt.
410func NewCancelRequest(cur realm, numbers string) Request {
411	mustBeLive()
412	var params []byte
413	var top uint64
414	if i := strings.Index(numbers, "-"); i >= 0 {
415		from, to := parseNumber(numbers[:i]), parseNumber(numbers[i+1:])
416		if to < from {
417			panic("hlgov: the range runs from the lower number to the higher")
418		}
419		params = append([]byte{1, 0}, be64(from)...)
420		params = append(params, be64(to)...)
421		top = to
422	} else {
423		var ns []uint64
424		for _, p := range strings.Split(numbers, ",") {
425			if strings.TrimSpace(p) == "" {
426				continue
427			}
428			n := parseNumber(p)
429			for _, x := range ns {
430				if x == n {
431					panic("hlgov: number " + strings.TrimSpace(p) + " named twice")
432				}
433			}
434			ns = append(ns, n)
435			if n > top {
436				top = n
437			}
438		}
439		if len(ns) == 0 || len(ns) > maxCancel {
440			panic("hlgov: a cancel names 1 to " + strconv.Itoa(maxCancel) + " numbers, or a range")
441		}
442		params = []byte{0, byte(len(ns))}
443		for _, n := range ns {
444			params = append(params, 0)
445			params = append(params, be64(n)...)
446		}
447	}
448	text := "cancel decisions " + numbers
449	mustFit(body(KindCancel, 0, append(make([]byte, 8), params...))) // with the salt's 8 bytes; dispatch checks it again
450	check := func() {
451		if top > last {
452			panic("hlgov: decision " + u64s(top) + " was never dispatched")
453		}
454	}
455	check()
456	return propose(cur, "cancel", text, func(cur realm, made, t int64) {
457		mustBeLive()
458		check()
459		p := append(salt(t), params...)
460		n := dispatch(cur, KindCancel, p, text, t)
461		note := "dispatched as decision " + u64s(n) + ", salt " + hx(p[:8])
462		// a cancelled kind 6 for a route of this chain clears the enrolment note it left, while that note is the
463		// latest (R37, AS2): the route's refund clock restarts with a re-proposal, instead of opening on a kind 6
464		// Solana never executed
465		eachCancelled(numbers, func(num uint64, d *decision) {
466			if d.kind != KindRouter || d.route == "" {
467				return
468			}
469			if h, _ := hlmailbox.Enrolled(d.route); h != d.height {
470				return
471			}
472			hlmailbox.ClearEnrolled(cross(cur), d.route)
473			note += "; the enrolment note of " + d.route + " cleared (kind 6 #" + u64s(num) + " cancelled): its refund clock restarts with a re-proposal"
474		})
475		d := ledger.Record("cancel", text, kindParams{kind: KindCancel, params: p}, made, t, t)
476		ledger.Apply(d, t, note)
477	})
478}
479
480// eachCancelled walks the decisions a cancel's numbers name — the list, or every dispatched number of the range —
481// in number order.
482func eachCancelled(numbers string, fn func(n uint64, d *decision)) {
483	if i := strings.Index(numbers, "-"); i >= 0 {
484		from, to := parseNumber(numbers[:i]), parseNumber(numbers[i+1:])
485		sent.Iterate(seqKey(from), seqKey(to+1), func(k string, v any) bool {
486			n, _ := strconv.ParseUint(strings.TrimLeft(k, "0"), 10, 64)
487			fn(n, v.(*decision))
488			return false
489		})
490		return
491	}
492	for _, p := range strings.Split(numbers, ",") {
493		if strings.TrimSpace(p) == "" {
494			continue
495		}
496		n := parseNumber(p)
497		if v := sent.Get(seqKey(n)); v != nil {
498			fn(n, v.(*decision))
499		}
500	}
501}
502
503// NewUnpauseRequest is kind 2: clear the flags set before its delivery on the pausable ISM (object 0) or the admin
504// program (object 1), 24 hours after it arrives, unless two guardians' flags hold.
505func NewUnpauseRequest(cur realm, object uint8) Request {
506	if object > 1 {
507		panic("hlgov: the object is 0 (the pausable ISM) or 1 (the admin program)")
508	}
509	name := "the pausable ISM"
510	if object == 1 {
511		name = "the admin program"
512	}
513	return proposeKind(cur, KindUnpause, "unpause", "unpause "+name, []byte{object}, func() {})
514}
515
516func parseToken(token uint8) string {
517	switch token {
518	case 0:
519		return "GNOT"
520	case 1:
521		return "BUBBLE"
522	}
523	panic("hlgov: the token is 0 (GNOT) or 1 (BUBBLE)")
524}
525
526// NewSetTokenISMRequest is kind 3: point a token (0 GNOT, 1 BUBBLE) at the pausable ISM, or at none (all zeros).
527func NewSetTokenISMRequest(cur realm, token uint8, ism string) Request {
528	name := parseToken(token)
529	k := mustKey(ism)
530	if k != mustKey(launchPausableISM) && k != ([32]byte{}) {
531		panic("hlgov: the ISM must be the pausable ISM " + launchPausableISM + " or all zeros")
532	}
533	params := append([]byte{token}, k[:]...)
534	return proposeKind(cur, KindTokenISM, "set token ISM", "set "+name+"'s ISM to "+hx(k[:]), params, func() {})
535}
536
537// NewSetClientLayoutRequest is kind 4, which §13 reserves (v27, AM7): the client's state layout the pausable ISM
538// and the immutable admin program read is fixed for the deployment's life (version 1), a client upgrade keeps it,
539// and a new layout is a successor admin program and pausable ISM by kind 9. The builder refuses every call, so no
540// kind-4 message ever leaves here; the admin program refuses one at execution too.
541func NewSetClientLayoutRequest(cur realm, target string, version uint8) Request {
542	panic("hlgov: kind 4 is reserved: the client's state layout is fixed for the deployment's life (version 1); a new layout is a successor by kind 9")
543}
544
545// parseSeatChanges reads "seat:pubkey" pairs (comma separated; the pubkey in base58 or hex): seats 1–6, two
546// guardian seats at most, four entries at most, no seat or key twice.
547func parseSeatChanges(changes string) ([]int, [][32]byte) {
548	var ns []int
549	var ks [][32]byte
550	g := 0
551	for _, p := range strings.Split(changes, ",") {
552		i := strings.Index(p, ":")
553		if i < 0 {
554			panic("hlgov: a change is seat:pubkey, not " + p)
555		}
556		n, err := strconv.Atoi(strings.TrimSpace(p[:i]))
557		if err != nil || n < 1 || n > seatCount {
558			panic("hlgov: the seat must be 1 to 6, not " + p[:i])
559		}
560		for _, x := range ns {
561			if x == n {
562				panic("hlgov: seat " + strconv.Itoa(n) + " named twice")
563			}
564		}
565		k := mustKey(p[i+1:])
566		if k == ([32]byte{}) {
567			panic("hlgov: a pubkey of zeros")
568		}
569		for _, x := range ks {
570			if x == k {
571				panic("hlgov: a pubkey named twice")
572			}
573		}
574		if n <= guardianSeats {
575			g++
576		}
577		ns = append(ns, n)
578		ks = append(ks, k)
579	}
580	if len(ns) == 0 || len(ns) > maxSeatChanges {
581		panic("hlgov: 1 to " + strconv.Itoa(maxSeatChanges) + " seats per decision")
582	}
583	if g > 2 {
584		panic("hlgov: two guardian seats at most per decision")
585	}
586	return ns, ks
587}
588
589// seatWarnings names the keys that hold a seat as this realm last set it. The real seats are Solana's, and the
590// admin program refuses a seat's current key itself (§6.5), spending the decision: a true no-op. The mirror here
591// can sit ahead of the truth (a kind 5 the admin program spent) or behind it (a kind 5 of another decision moved a seat),
592// so it is no ground to refuse a proposal — the builder warns in the description and the member reads
593// `hlwatch`'s list of the seats as they stand.
594func seatWarnings(ns []int, ks [][32]byte) string {
595	var parts []string
596	for i := range ns {
597		for s := 1; s <= seatCount; s++ {
598			if solSeats[s] == ks[i] {
599				parts = append(parts, hx(ks[i][:])+" holds seat "+strconv.Itoa(s)+" as last set here")
600			}
601		}
602	}
603	if len(parts) == 0 {
604		return ""
605	}
606	return " (warning: " + strings.Join(parts, "; ") + " — the admin program spends a decision naming a seat's current key, so check the seats as they stand on Solana before voting)"
607}
608
609// NewSetGuardiansRequest is kind 5: replace guardian seats (1–4, two at most) and watch slots (5–6) on Solana,
610// four entries at most. A key that holds a seat as this realm last set it is warned about in the description,
611// not refused: the admin program's own check spends a decision naming a seat's current key, and this realm's
612// mirror of the seats can drift from the truth. It lands 7 days after delivery through any flag or hold.
613func NewSetGuardiansRequest(cur realm, changes string) Request {
614	mustBeLive()
615	ns, ks := parseSeatChanges(changes)
616	params := []byte{byte(len(ns))}
617	parts := make([]string, len(ns))
618	for i, n := range ns {
619		params = append(params, byte(n))
620		params = append(params, ks[i][:]...)
621		parts[i] = "seat " + strconv.Itoa(n) + " to " + hx(ks[i][:])
622	}
623	text := "set guardians and watch keys: " + strings.Join(parts, ", ") + seatWarnings(ns, ks)
624	return propose(cur, "set guardians", text, func(cur realm, made, t int64) {
625		mustBeLive()
626		n := dispatch(cur, KindGuardians, params, text, t)
627		for i, s := range ns {
628			solSeats[s] = ks[i]
629		}
630		d := ledger.Record("set guardians", text, kindParams{kind: KindGuardians, params: params, seats: ns, keys: ks}, made, t, t)
631		ledger.Apply(d, t, "dispatched as decision "+u64s(n))
632	})
633}
634
635// NewEnrollRouterRequest is kind 6: enroll a token's remote router for a domain. For gno.land's own domain the
636// router must be a route realm registered with hlmailbox — the launch routes, and the successors their migrations
637// named — as 32 bytes: the synthetic program takes every Dispatch from the router it enrolls as a mint order, and
638// hlmailbox's Dispatch is open to any realm, so any other address would mint synthetic that no lock backs; the
639// token's own route (R36), unmigrated, and — a successor — one that pulled page 0 (R35). The rules are checked
640// when the proposal is made and again when it executes. The closure then notes with the mailbox the route it
641// enrolled and the height (hlmailbox.NoteEnrolled, AR1): that route's refund builder waits 14 days after the note
642// (R37: a week for the delivery, a week for Solana's clock), since a refunded send stays deliverable on Solana
643// until the predecessor's router is replaced; a cancel of the kind 6 clears the note (NewCancelRequest).
644func NewEnrollRouterRequest(cur realm, token uint8, domain uint32, router string) Request {
645	mustBeLive()
646	name := parseToken(token)
647	k := mustKey(router)
648	if k == ([32]byte{}) || domain == 0 {
649		panic("hlgov: the router and the domain must be set")
650	}
651	local := domain == hlmailbox.LocalDomain()
652	check := func() string {
653		if !local {
654			return ""
655		}
656		path := routePathOf(k)
657		if path == "" {
658			panic("hlgov: for this chain's domain the router must be a route realm registered with hlmailbox, not " + hx(k[:]))
659		}
660		// the token's own route, and a live one (R36): the other token's route — or its successor — as this
661		// token's router strands this token's burns behind per-id votes until a corrective kind 6 lands, refuses
662		// its sends on Solana meanwhile, and writes the enrolment note on the other route, opening its refunds
663		// while the other token's router still delivers (AR1's double mint again); a migrated route's successor
664		// is the token's router, never the route itself
665		if sym := hlmailbox.RouteSymbol(path); sym == "" {
666			panic("hlgov: " + path + " is registered for no token (it never announced the route it succeeds): a kind 6 names the token's own route")
667		} else if sym != name {
668			panic("hlgov: " + path + " is the " + sym + " route's, not " + name + "'s: a kind 6 names the token's own route")
669		}
670		if s := hlmailbox.Successor(path); s != "" {
671			panic("hlgov: " + path + " migrated to " + s + ": never a migrated route, since its successor is the token's router")
672		}
673		// a successor that pulled nothing is never enrolled (R35): before page 0 its migration may still be
674		// re-pointed (AR2) and the route dropped — a realm in nobody's chain that Solana would then name the
675		// router for the deployment's life, every burn's recipient, flaggable by nobody
676		if pred := hlmailbox.Predecessor(path); pred != "" && !hlmailbox.Pulled(path) {
677			panic("hlgov: " + path + " succeeds " + pred + " and has pulled nothing yet (page 0 is not in): a kind 6 enrolling it waits for its first page, since a migrate that pulled nothing may be re-pointed and Solana must never name a dropped route")
678		}
679		return path
680	}
681	params := append(append([]byte{token}, be32(domain)...), k[:]...)
682	text := "enroll " + name + "'s router for domain " + strconv.FormatUint(uint64(domain), 10) + ": " + hx(k[:])
683	mustFit(body(KindRouter, 0, params))
684	check()
685	return propose(cur, "enroll router", text, func(cur realm, made, t int64) {
686		mustBeLive()
687		path := check()
688		n := dispatch(cur, KindRouter, params, text, t)
689		note := "dispatched as decision " + u64s(n)
690		if path != "" {
691			h := runtime.ChainHeight()
692			hlmailbox.NoteEnrolled(cross(cur), path, h)
693			sd := sent.Get(seqKey(n)).(*decision)
694			sd.route, sd.height = path, h
695			note += "; " + path + " noted enrolled on Solana at height " + itoa(h)
696		}
697		d := ledger.Record("enroll router", text, kindParams{kind: KindRouter, params: params}, made, t, t)
698		ledger.Apply(d, t, note)
699	})
700}
701
702// routePathOf is the pkgpath of the route realm hlmailbox registers whose address, as 32 bytes, is k; "" if none.
703func routePathOf(k [32]byte) string {
704	for _, path := range hlmailbox.Routes() {
705		if b, err := hl.AddressToBytes32(chain.PackageAddress(path)); err == nil && b == k {
706			return path
707		}
708	}
709	return ""
710}
711
712// NewSetProtocolFeeRequest is kind 7: the mailbox's protocol fee (lamports, at most its 0.05 SOL maximum) and its
713// beneficiary (the treasurer's Solana key).
714func NewSetProtocolFeeRequest(cur realm, lamports uint64, beneficiary string) Request {
715	if lamports > maxProtocolFee {
716		panic("hlgov: the fee is at most " + u64s(maxProtocolFee) + " lamports, the mailbox's maximum")
717	}
718	k := mustKey(beneficiary)
719	if k == ([32]byte{}) {
720		panic("hlgov: a beneficiary of zeros")
721	}
722	params := append(be64(lamports), k[:]...)
723	return proposeKind(cur, KindProtocolFee, "set protocol fee", "set the protocol fee to "+u64s(lamports)+" lamports for "+hx(k[:]), params, func() {})
724}
725
726// NewUpgradeRequest is kind 8: upgrade a program from a staged buffer whose ELF hashes to elfHash (sha256, hex) —
727// never the admin program's own id (§13: its upgrade authority is burned, so the decision could only be spent; a
728// new admin program is a successor by kind 9), checked when the proposal is made and when it executes.
729func NewUpgradeRequest(cur realm, program, buffer, elfHash string) Request {
730	p, b, h := mustKey(program), mustKey(buffer), mustKey(elfHash)
731	if p == ([32]byte{}) || b == ([32]byte{}) || h == ([32]byte{}) || p == b {
732		panic("hlgov: the program, the buffer and the hash must be set and distinct")
733	}
734	check := func() {
735		if p == recipient {
736			panic("hlgov: never the admin program's own id (" + hx(p[:]) + "): it is immutable, a new admin program is a successor by kind 9")
737		}
738	}
739	params := append(append(append([]byte{}, p[:]...), b[:]...), h[:]...)
740	return proposeKind(cur, KindUpgrade, "upgrade", "upgrade "+hx(p[:])+" from buffer "+hx(b[:])+" with ELF sha256 "+hx(h[:]), params, check)
741}
742
743// NewTransferToSuccessorRequest is kind 9: hand every authority to an immutable successor admin program — never
744// the admin program itself, the decision recipient this realm knows, checked when the proposal is made and again
745// when it executes (a recipient swap landing in between would otherwise turn a pre-armed transfer into one the
746// admin program only spends: it re-points every authority to itself and dies on its own SuccessorInit, the
747// programs lane's S-15), as kind 8 refuses its self-upgrade; and never a former recipient (R29), a retired program
748// whose successor_init answers NotInherited. The admin program refuses a successor whose programdata shows an
749// upgrade authority; the builder cannot read Solana, so a member checks that by RPC before proposing.
750func NewTransferToSuccessorRequest(cur realm, program string) Request {
751	p := mustKey(program)
752	if p == ([32]byte{}) {
753		panic("hlgov: the successor must be set")
754	}
755	check := func() {
756		if p == recipient {
757			panic("hlgov: never the admin program itself (" + hx(p[:]) + "), not the current admin program as its own successor: the admin program would only spend the decision")
758		}
759		mustNotBeFormer(p, "admin program")
760	}
761	return proposeKind(cur, KindSuccessor, "transfer to successor", "transfer every authority to "+hx(p[:]), p[:], check)
762}
763
764// routeMaximum is a token's per-message maximum on its gno.land route now (base units): what a send may carry out.
765func routeMaximum(token uint8) uint64 {
766	if token == 0 {
767		return uint64(hlgnot.Maximum())
768	}
769	return uint64(hlbubble.Maximum())
770}
771
772// NewSetMintLimitsRequest is kind 12: a token's (0 GNOT, 1 BUBBLE) per-message maximum and rolling day cap on the
773// pausable ISM, in base units (§13: token u8 ‖ maximum u64 ‖ day cap u64; compiled there at launch as 25,000 a
774// message and 100,000 a day of each token). A maximum of 0 (every mint refused) and a day cap below the maximum (a
775// message at the maximum could never pass) are refused when the proposal is made: the admin program and the
776// pausable ISM take any values, so the nonsense is caught here, as every builder catches it early. The maximum is
777// never below the token's route's per-message maximum on gno.land (R15), checked when the proposal is made and
778// when it executes: a send between the two would leave gno.land and be refused at delivery for good, stranded
779// until a refund by id — so a raise is the Solana limit first (this), the route's maximum after, and a lowering
780// the other way round. It lands 7 days after delivery.
781func NewSetMintLimitsRequest(cur realm, token uint8, maximum, dayCap uint64) Request {
782	name := parseToken(token)
783	if maximum == 0 {
784		panic("hlgov: the per-message maximum is at least 1")
785	}
786	if dayCap < maximum {
787		panic("hlgov: the day cap is at least the per-message maximum")
788	}
789	check := func() {
790		if rm := routeMaximum(token); maximum < rm {
791			panic("hlgov: a per-message maximum of " + u64s(maximum) + " is below the " + name + " route's maximum of " + u64s(rm) + " (base units): a send between the two would strand on Solana — lower the route's maximum first, then this")
792		}
793	}
794	params := append([]byte{token}, be64(maximum)...)
795	params = append(params, be64(dayCap)...)
796	text := "set " + name + "'s mint limits: " + u64s(maximum) + " a message, " + u64s(dayCap) + " a day (base units)"
797	return proposeKind(cur, KindMintLimits, "set mint limits", text, params, check)
798}
799
800// mustRaise refuses a sync sequence that would not raise the counter (§5.3: it never lowers it): n is the number
801// the next decision takes, so the counter becomes n-1, which must be above the counter now — and by at most 2^32,
802// the realm's own bound (§13, R18; kind 10 is reserved since v35 and moves nothing on Solana), so a mistyped
803// number can neither wrap the counter onto numbers the admin program recorded nor put every decision beyond the
804// next number it expects.
805func mustRaise(n uint64) {
806	if n == last+1 {
807		panic("hlgov: the next number is " + u64s(n) + " already")
808	}
809	if n <= last {
810		panic("hlgov: sync sequence only ever raises the counter: it stands at " + u64s(last) + " (the next number is " + u64s(last+1) + ") and " + u64s(n) + " is not above it")
811	}
812	if n > last+1+syncReach {
813		panic("hlgov: sync sequence reaches at most 2^32 above the next number (the realm's own bound): the next number is " + u64s(last+1) + " and " + u64s(n) + " is beyond " + u64s(last+1+syncReach))
814	}
815}
816
817// NewSyncSequenceRequest raises this realm's counter so that the next decision takes number n — the number the
818// admin program expects next after a foreign decision took one — at once; nothing moves on Solana. It only ever
819// raises: a number at or below the next one is refused when the proposal is made and again when it executes (the
820// counter may have moved in between), so a number the admin program recorded is never reused.
821func NewSyncSequenceRequest(cur realm, n uint64) Request {
822	mustBeLive()
823	if n == 0 {
824		panic("hlgov: the next number is at least 1")
825	}
826	mustRaise(n)
827	text := "sync sequence: the next decision takes number " + u64s(n) + " (it was " + u64s(last+1) + ")"
828	return propose(cur, "sync sequence", text, func(cur realm, made, t int64) {
829		mustBeLive()
830		mustRaise(n)
831		last = n - 1
832		d := ledger.Record("sync sequence", text, sequenceParams{n}, made, t, t)
833		ledger.Apply(d, t, "at once")
834		chain.Emit("SequenceSynced", "next", u64s(n))
835	})
836}
837
838// NewSetDecisionRecipientRequest changes the program decisions are addressed to, 7 days after the proposal
839// executes: how a successor admin program receives them. Never the current recipient, and never a former one
840// (R29: a retired program answers Retired to every decision) — checked when the proposal is made, in its closure
841// and when the queued change would apply.
842func NewSetDecisionRecipientRequest(cur realm, program string) Request {
843	mustBeLive()
844	p := mustKey(program)
845	if p == ([32]byte{}) {
846		panic("hlgov: the recipient must be set")
847	}
848	check := func() {
849		if p == recipient {
850			panic("hlgov: the recipient must differ from the current one (" + hx(p[:]) + ")")
851		}
852		mustNotBeFormer(p, "decision recipient")
853	}
854	check()
855	text := "set decision recipient to " + hx(p[:]) + " (now " + hx(recipient[:]) + ")"
856	return propose(cur, "set decision recipient", text, func(cur realm, made, t int64) {
857		mustBeLive()
858		check()
859		d := ledger.Record("set decision recipient", text, recipientParams{p}, made, t, t+decisions.Delay)
860		d.Note = "queued 7 days"
861	})
862}
863
864// NewCancelQueuedRequest cancels a queued change of this realm (a recipient swap), at once; it is proposed before
865// Launch too.
866func NewCancelQueuedRequest(cur realm, id int64) Request {
867	d := ledger.Get(id)
868	if d == nil || !d.Pending() {
869		panic("hlgov: decision " + itoa(id) + " is not pending")
870	}
871	text := "cancel queued change " + itoa(id) + " (" + d.Text + ")"
872	return propose(cur, "cancel queued", text, func(cur realm, made, t int64) {
873		if _, err := ledger.Cancel(id, t); err != nil {
874			panic("hlgov: " + err.Error())
875		}
876		c := ledger.Record("cancel queued", text, cancelParams{id}, made, t, t)
877		ledger.Apply(c, t, "at once: cancelled")
878	})
879}
880
881// Execute applies a queued change (a recipient swap) whose 7 days have passed; anyone may call it. A swap decided
882// while its target was nobody applies only while that holds (R29): one that finds its target the recipient already
883// (a second swap to the same program) or a former recipient (replaced since) is refused and stays pending for
884// GovDAO's cancel; the recipient it replaces joins the former ones.
885func Execute(cur realm, id int64) {
886	caller(cur)
887	mustBeLive()
888	t := now()
889	d, err := ledger.Due(id, t)
890	if err != nil {
891		panic("hlgov: " + err.Error())
892	}
893	p, ok := d.Params.(recipientParams)
894	if !ok {
895		panic("hlgov: decision " + itoa(id) + " has nothing to apply")
896	}
897	if p.to == recipient {
898		panic("hlgov: " + hx(p.to[:]) + " is the decision recipient already: cancel decision " + itoa(id))
899	}
900	mustNotBeFormer(p.to, "decision recipient")
901	former = append(former, recipient)
902	recipient = p.to
903	ledger.Apply(d, t, "applied after the delay")
904	chain.Emit("RecipientSet", "program", hx(recipient[:]))
905}
906
907// --------------------------------------------------------------- pages --
908
909// Render: "" the page; "json"; "decisions" and "decisions.json" the ledger; "decision/<n>" one dispatched decision.
910func Render(path string) string {
911	switch {
912	case path == "":
913		var b strings.Builder
914		b.WriteString("# GovDAO's decisions for Solana\n\n")
915		b.WriteString("A decision made with one of this realm's builders — the governor's own until it yields, a GovDAO proposal's execution after — sends it to the admin program on Solana with the next sequence number; the admin program records it by number and executes it after its clock. Anyone may re-dispatch an executed decision until it is recorded.\n\n")
916		if hlmailbox.Live() {
917			b.WriteString("- **live** since " + itoa(hlmailbox.LaunchedAt()) + "\n")
918		} else {
919			b.WriteString("- **not launched**: no decision is sent before the Launch decision\n")
920		}
921		b.WriteString("- " + decisions.GovernorLine(launchGovernor, hlmailbox.Yielded()) + "\n")
922		b.WriteString("- destination: domain " + strconv.FormatUint(uint64(destination), 10) + ", recipient `" + hx(recipient[:]) + "`\n")
923		if len(former) > 0 {
924			parts := make([]string, len(former))
925			for i, k := range former {
926				parts[i] = "`" + hx(k[:]) + "`"
927			}
928			b.WriteString("- former recipients (retired; never a successor, never the recipient again): " + strings.Join(parts, ", ") + "\n")
929		}
930		b.WriteString("- launch client: `" + hx(client[:]) + "` (the pausable ISM's target; its state layout is fixed for the deployment's life at version 1 — kind 4 is reserved and refused here)\n")
931		b.WriteString("- sequence: starts at " + u64s(launchSequence) + " (compiled: the predecessor's last number, 0 for a chain's first r/hlgov); last number " + u64s(last) + "; the next takes number " + u64s(last+1) + "; " + strconv.Itoa(sent.Size()) + " sent from here\n")
932		b.WriteString("- " + decisions.Clocks() + " (the recipient swap's delay and every builder's deadline; Solana's clocks are the admin program's)\n")
933		b.WriteString("- Solana seats as last set here:\n")
934		for i := 1; i <= seatCount; i++ {
935			kind := "guardian"
936			if i > guardianSeats {
937				kind = "watch key"
938			}
939			b.WriteString("  - seat " + strconv.Itoa(i) + " (" + kind + "): `" + hx(solSeats[i][:]) + "`\n")
940		}
941		if sent.Size() > 0 {
942			b.WriteString("\n## Sent\n\n")
943			sent.ReverseIterateByOffset(0, 20, func(k string, v any) bool {
944				d := v.(*decision)
945				b.WriteString("- #" + strings.TrimLeft(k, "0") + " kind " + strconv.Itoa(int(d.kind)) + ": " + d.text + " (sent " + strconv.Itoa(d.count) + "×, last id `" + d.id + "`)\n")
946				return false
947			})
948		}
949		b.WriteString("\n## Decisions\n\n" + ledger.Page())
950		return b.String()
951	case path == "json":
952		fs := Former()
953		for i := range fs {
954			fs[i] = strconv.Quote(fs[i])
955		}
956		return "{\"address\":" + strconv.Quote(string(Address())) + ",\"live\":" + strconv.FormatBool(hlmailbox.Live()) +
957			"," + decisions.GovernorJSON(launchGovernor, hlmailbox.Yielded()) +
958			",\"destination\":" + strconv.FormatUint(uint64(destination), 10) + ",\"recipient\":" + strconv.Quote(hx(recipient[:])) + ",\"former\":[" + strings.Join(fs, ",") + "]" +
959			",\"start\":" + u64s(launchSequence) + ",\"last\":" + u64s(last) + ",\"next\":" + u64s(last+1) + ",\"clocks\":" + decisions.ClocksJSON() + ",\"decisions\":" + itoa(ledger.Last()) + "}"
960	case path == "decisions":
961		return "# Decisions\n\n" + ledger.Page()
962	case path == "decisions.json":
963		return ledger.JSON()
964	case strings.HasPrefix(path, "decision/"):
965		n, err := strconv.ParseUint(path[len("decision/"):], 10, 64)
966		if err != nil {
967			return "null"
968		}
969		v := sent.Get(seqKey(n))
970		if v == nil {
971			return "null"
972		}
973		d := v.(*decision)
974		return "{\"number\":" + u64s(n) + ",\"kind\":" + strconv.Itoa(int(d.kind)) + ",\"body\":" + strconv.Quote(hx(d.body)) +
975			",\"id\":" + strconv.Quote(d.id) + ",\"count\":" + strconv.Itoa(d.count) + ",\"at\":" + itoa(d.at) + ",\"text\":" + strconv.Quote(d.text) + "}"
976	}
977	return "unknown path; see the page"
978}