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}