gov.gno
26.83 Kb · 664 lines
1package hlmailbox
2
3import (
4 "chain"
5 "chain/runtime"
6 "errors"
7 "strconv"
8 "strings"
9
10 "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/decisions/v0"
11 "gno.land/r/gov/dao"
12)
13
14// The builders (bridge/PLAN.md §5.3): the only way a setting of this realm changes is a decision made with one of
15// the New…Request functions — before the governor yields, the governor's own `maketx run` script (M1k):
16//
17// hlmailbox.Govern(cross(cur), hlmailbox.NewSetVerifierRequest(cross(cur), origin, path))
18//
19// and after it a GovDAO proposal from a member's:
20//
21// req := hlmailbox.NewSetVerifierRequest(cross(cur), origin, path)
22// dao.MustCreateProposal(cross(cur), req.Proposal())
23//
24// A builder takes no free text: it checks its parameters, writes them as the proposal's description, and returns
25// a Request whose closure runs when the decision is made — the one closure, whether the governor runs it through
26// Govern or GovDAO's proxy runs it when the proposal executes. The closure checks who decides (mustBeGovernor: the
27// governor's own call until the yield, gno.land/r/gov/dao's proxy after), returns an error only when the request
28// is past its 14-day deadline (GovDAO then denies the proposal for good; Govern panics with it) and panics on
29// anything else (a proposal survives for a retry), gives the decision a number, and decides the at-once test
30// against the live state: a removal or an unpause applies at once; a swap or an addition is queued 7 days and
31// anyone applies it with Execute(id) after that. Launch is the one-shot that makes the realms live; it carries the
32// same 14-day deadline (a passed launch nobody executed in 14 days lapses and is re-proposed, never a standing
33// trap); before it every builder but Launch and cancel is refused.
34
35// Request is what a builder returns (M1k): the proposal GovDAO's proxy executes after the yield (Proposal), and
36// the same decision the governor runs directly before it (Govern). The closure is one; only who may run it
37// changes with the yield. A Request comes from a builder alone — the type is this realm's and the closure
38// unexported, so no other code can build one or change what it decides (the construction and conversion guards of
39// the interrealm spec, §3.2 and §8.2) — and a stranger holding one can only hand it back to Govern, which refuses
40// everyone but the governor.
41type Request struct {
42 proposal dao.ProposalRequest
43 text string
44 decide func(cur realm) error
45}
46
47// Proposal is the request as a GovDAO proposal: `dao.MustCreateProposal(cross(cur), req.Proposal())`.
48func (r Request) Proposal() dao.ProposalRequest { return r.proposal }
49
50// Description is the proposal's: the parameters as the builder wrote them.
51func (r Request) Description() string { return r.text }
52
53type (
54 launchParams struct{}
55 setVerifierParams struct {
56 origin uint32
57 path string
58 }
59 rotateSignersParams struct {
60 keys []Key
61 rule Rule
62 // A queued rotation keeps the signer set and rule it was decided against; Execute applies it only while
63 // they still stand, so a removal made in between (a compromised key cut at once) can never be undone by
64 // an older proposal landing on its 7th day — GovDAO proposes afresh against the set as it is.
65 oldKeys []Key
66 oldRule Rule
67 }
68 rotateSeatsParams struct {
69 seats []int
70 addrs []address // "" removes the seat
71 held []address // each seat's holder when the proposal was made: a removal of a seat whose occupant changed since is skipped (R21, as the void's and the unpause's, R17)
72 before [seatCount + 1]address // the six seats as they stood when the proposal executed (a queued change applies only while they stand)
73 }
74 unpauseParams struct {
75 route string
76 fns []string
77 removeSeat int
78 removeAddr address // the seat's holder when the proposal was made: the seat is vacated only while it still holds it (R17)
79 }
80 cancelParams struct{ id int64 }
81)
82
83// who names a previous realm in a refusal: its pkgpath, or a user's address.
84func who(path string, a address) string {
85 if path != "" {
86 return path
87 }
88 return string(a)
89}
90
91// mustBeGovernor is a decision's identity check (M1k), read from the closure's own live cur: until the yield the
92// governor's own call — the compiled address, by its own transaction (cur.Previous() a user realm: a MsgCall's, or
93// a MsgRun script's ephemeral realm, whose address is the signer's; never another realm), which is Govern's
94// non-crossing call of the closure — and gno.land/r/gov/dao's proposals refused; after it, or when no governor
95// was compiled, the proxy's exact pkgpath, as before M1k, and the governor refused like anyone. A refusal is a
96// panic: a proposal made before the yield survives for a retry after it, within its 14 days.
97func mustBeGovernor(cur realm) {
98 prev := cur.Previous()
99 if decisions.DAOGoverns(launchGovernor, yielded) {
100 if p := prev.PkgPath(); p != decisions.DAOPath {
101 panic("hlmailbox: only " + decisions.DAOPath + " executes a decision, not " + who(prev.PkgPath(), prev.Address()))
102 }
103 return
104 }
105 if !prev.IsUser() || prev.Address() != launchGovernor {
106 panic("hlmailbox: until the governor yields, " + string(launchGovernor) + " alone decides, by Govern from its own key: not " + who(prev.PkgPath(), prev.Address()) + " (a GovDAO proposal is refused until the yield)")
107 }
108}
109
110func mustBeLive() {
111 if !live {
112 panic("hlmailbox: not launched: before the Launch decision only Launch and cancel are proposed")
113 }
114}
115
116// propose wraps a builder's effect in the closure every builder shares. apply runs at the decision with the time
117// the request was made and the time now; it records the decision.
118func propose(cur realm, kind, text string, apply func(made, t int64)) Request {
119 if !cur.IsCurrent() {
120 panic("hlmailbox: that realm value is not live; pass your own cur")
121 }
122 made := now()
123 cb := func(cur realm) error {
124 mustBeGovernor(cur)
125 t := now()
126 if err := decisions.Expired(made, t); err != nil {
127 return err
128 }
129 apply(made, t)
130 return nil
131 }
132 return request(cur, kind, text, cb)
133}
134
135// request is a builder's Request: the proposal around the closure for GovDAO's path, the closure itself for the
136// governor's.
137func request(cur realm, kind, text string, cb func(cur realm) error) Request {
138 return Request{proposal: dao.NewProposalRequest("hlmailbox: "+kind, text, dao.NewSimpleExecutor(0, cur, cb, text)), text: text, decide: cb}
139}
140
141// Govern is the governor's way in before the yield (M1k): the governor — the compiled launchGovernor, by its own
142// transaction (a MsgCall, or the MsgRun script that makes the request in the same transaction; never through
143// another realm) — makes a builder's decision directly, the same closure GovDAO's proxy runs after the yield: the
144// same checks, the same at-once test, the same 7-day queue and 14-day deadline, the same record and events. The
145// closure is called non-crossing, in this frame, so it reads the governor from cur.Previous() as it reads the
146// proxy after the yield. Refused for anyone else, and for everyone once the governor yielded (or when none was
147// compiled): GovDAO's proposals decide then. A request past its 14 days is refused (the deadline error, as a
148// proposal's denial) and made afresh.
149func Govern(cur realm, r Request) {
150 if !cur.IsCurrent() {
151 panic("hlmailbox: that realm value is not live; pass your own cur")
152 }
153 if r.decide == nil {
154 panic("hlmailbox: not a builder's request")
155 }
156 if decisions.DAOGoverns(launchGovernor, yielded) {
157 panic("hlmailbox: " + governedBy() + ": a decision is a GovDAO proposal, never Govern")
158 }
159 mustBeGovernor(cur)
160 if err := r.decide(cur); err != nil {
161 panic("hlmailbox: " + err.Error())
162 }
163}
164
165// governedBy says why Govern is closed: the yield's height, or no governor compiled.
166func governedBy() string {
167 if launchGovernor == "" {
168 return "no governor was compiled, " + decisions.DAOPath + " governs from the first block"
169 }
170 return "the governor yielded at height " + strconv.FormatInt(yielded, 10) + ", " + decisions.DAOPath + " governs since"
171}
172
173// Yield is the governor's one-way hand-over to GovDAO (M1k): from this block gno.land/r/gov/dao's proposals are
174// the only way a decision is made on every bridge realm — each reads Yielded here before it accepts one, so no
175// realm is ever yielded apart from the rest — and Govern is closed for good. The governor alone, by its own
176// transaction; refused when no governor was compiled (GovDAO governs from the first block) and a second time. A
177// decision the governor queued still applies by Execute(id) after its 7 days, or GovDAO cancels it.
178func Yield(cur realm) {
179 caller(cur)
180 if launchGovernor == "" {
181 panic("hlmailbox: no governor was compiled: " + decisions.DAOPath + " governs from the first block")
182 }
183 if yielded != 0 {
184 panic("hlmailbox: the governor yielded at height " + strconv.FormatInt(yielded, 10) + " already")
185 }
186 prev := cur.Previous()
187 if !prev.IsUser() || prev.Address() != launchGovernor {
188 panic("hlmailbox: only the governor " + string(launchGovernor) + " yields, not " + who(prev.PkgPath(), prev.Address()))
189 }
190 yielded = runtime.ChainHeight() // never 0: the chain's first executing block is height 1
191 chain.Emit("Yielded", "governor", string(launchGovernor), "height", strconv.FormatInt(yielded, 10))
192}
193
194func queue(kind, text string, params any, made, t int64) *decisions.Decision {
195 d := ledger.Record(kind, text, params, made, t, t+decisions.Delay)
196 d.Note = "queued 7 days"
197 chain.Emit("Decision", "id", strconv.FormatInt(d.ID, 10), "kind", kind, "state", "queued")
198 return d
199}
200
201func atOnce(kind, text string, params any, made, t int64, note string) *decisions.Decision {
202 d := ledger.Record(kind, text, params, made, t, t)
203 ledger.Apply(d, t, "at once: "+note)
204 chain.Emit("Decision", "id", strconv.FormatInt(d.ID, 10), "kind", kind, "state", "applied")
205 return d
206}
207
208// ------------------------------------------------------------- launch --
209
210// NewLaunchRequest is the launch's builder — the governor's decision like any other (M1k), the launch vote's once
211// the governor has yielded: its execution makes the realms live, once, with the compiled values. It carries the
212// 14-day deadline every builder carries: a passed launch nobody executed within 14 days of its proposal returns
213// the deadline error (GovDAO denies it for good) and the launch is proposed again, so a forgotten vote is never a
214// standing trap. A second execution errors.
215func NewLaunchRequest(cur realm) Request {
216 if !cur.IsCurrent() {
217 panic("hlmailbox: that realm value is not live; pass your own cur")
218 }
219 if live {
220 panic("hlmailbox: launched already")
221 }
222 made := now()
223 text := "launch: the compiled values go live — verifier " + launchVerifier + " for origin " + u32(launchOrigin) +
224 ", signers " + launchSigners + " under the rule " + Rule{Threshold: launchThreshold}.String() +
225 ", routes " + launchRouteGNOT + " and " + launchRouteBUBBLE + ", seats " + seatsText()
226 cb := func(cur realm) error {
227 mustBeGovernor(cur)
228 t := now()
229 if err := decisions.Expired(made, t); err != nil {
230 return err
231 }
232 if live {
233 return errors.New("hlmailbox: launched already")
234 }
235 live, launchedAt = true, t
236 atOnce("launch", text, launchParams{}, made, t, "live")
237 chain.Emit("Launch", "at", strconv.FormatInt(t, 10))
238 return nil
239 }
240 return request(cur, "launch", text, cb)
241}
242
243func seatsText() string {
244 parts := make([]string, 0, seatCount)
245 for i := 1; i <= seatCount; i++ {
246 parts = append(parts, strconv.Itoa(i)+":"+string(seats[i]))
247 }
248 return strings.Join(parts, ",")
249}
250
251// ------------------------------------------------------------- verifier --
252
253// NewSetVerifierRequest activates a registered verifier for an origin, 7 days after the proposal executes.
254func NewSetVerifierRequest(cur realm, origin uint32, path string) Request {
255 mustBeLive()
256 if origin == localDomain {
257 panic("hlmailbox: this chain's own domain has no verifier")
258 }
259 if !verifiers.Has(path) {
260 panic("hlmailbox: no verifier registered at " + path)
261 }
262 if ActiveVerifier(origin) == path {
263 panic("hlmailbox: " + path + " is the active verifier for origin " + u32(origin) + " already")
264 }
265 text := "set verifier for origin " + u32(origin) + " to " + path
266 return propose(cur, "set verifier", text, func(made, t int64) {
267 mustBeLive()
268 if !verifiers.Has(path) {
269 panic("hlmailbox: no verifier registered at " + path)
270 }
271 queue("set verifier", text, setVerifierParams{origin, path}, made, t)
272 })
273}
274
275// -------------------------------------------------------------- signers --
276
277// parseSigners reads a signer set and its rule: keys in hex, comma separated; a threshold, or both groups (comma
278// separated hex, subsets of the keys) with threshold 0.
279func parseSigners(keys string, threshold int, groupA, groupB string) ([]Key, Rule) {
280 ks := parseKeys(keys)
281 if len(ks) == 0 {
282 panic("hlmailbox: no signers")
283 }
284 a, b := parseKeys(groupA), parseKeys(groupB)
285 if (len(a) == 0) != (len(b) == 0) {
286 panic("hlmailbox: name both groups or neither")
287 }
288 if len(a) > 0 {
289 if threshold != 0 {
290 panic("hlmailbox: a two-group rule takes no threshold")
291 }
292 if !subset(a, ks) || !subset(b, ks) {
293 panic("hlmailbox: every group member must be a signer")
294 }
295 for _, k := range a {
296 if indexOf(b, k) >= 0 {
297 panic("hlmailbox: a key in both groups: " + hx(k[:]))
298 }
299 }
300 return ks, Rule{GroupA: a, GroupB: b}
301 }
302 if threshold < 1 || threshold > len(ks) {
303 panic("hlmailbox: the threshold must be between 1 and the number of signers")
304 }
305 return ks, Rule{Threshold: threshold}
306}
307
308// pruned is the live rule with every key absent from ks dropped from both groups: what a pure removal leaves of
309// the rule. A threshold is not pruned (§5.3: a 2-of-3 rule is still met by the 2 that remain after one removal);
310// a group is (one of A and one of B is still met by the members that remain). Removing a partner — the only
311// member of its group — leaves a group empty, which no proposal names, so that removal is never at once.
312func pruned(ks []Key) Rule {
313 r := Rule{Threshold: signerRule.Threshold}
314 for _, k := range signerRule.GroupA {
315 if indexOf(ks, k) >= 0 {
316 r.GroupA = append(r.GroupA, k)
317 }
318 }
319 for _, k := range signerRule.GroupB {
320 if indexOf(ks, k) >= 0 {
321 r.GroupB = append(r.GroupB, k)
322 }
323 }
324 return r
325}
326
327// NewRotateSignersRequest sets the signer set and its rule together (§5.3): the keys in hex, comma separated, in
328// the order signatures name them; a threshold, or §11.13's two groups (one of each must sign; threshold 0). It
329// applies at once only as a pure removal — the new set a subset of the old, under the live rule pruned of the
330// removed keys (the same threshold; the groups less the keys that leave), which the active verifier says the
331// remaining set can still meet (Satisfiable, decided when the proposal executes); any other change, a lowered
332// threshold among them, is queued 7 days, and a queued rotation applies only while the signer set and rule are
333// still those it was decided against.
334func NewRotateSignersRequest(cur realm, keys string, threshold int, groupA, groupB string) Request {
335 mustBeLive()
336 ks, rule := parseSigners(keys, threshold, groupA, groupB)
337 if sameKeys(ks, signerKeys) && rule.equal(signerRule) {
338 panic("hlmailbox: nothing changes")
339 }
340 // a pure removal is checked against the active verifier's Satisfiable when it is made too (K19): one that
341 // would leave the rule unsatisfiable is nonsense, refused early — at execution the test is made again, and a
342 // removal it fails then (another landed in between) is queued, never refused (§5.3)
343 if rule.sameAs(pruned(ks)) && len(ks) < len(signerKeys) && subset(ks, signerKeys) && !satisfiable(ks) {
344 panic("hlmailbox: the active verifier says the signers that remain cannot meet the rule: a removal never leaves the rule unsatisfiable")
345 }
346 text := "rotate signers to " + keysHex(ks) + " under the rule " + rule.String()
347 return propose(cur, "rotate signers", text, func(made, t int64) {
348 mustBeLive()
349 p := rotateSignersParams{keys: ks, rule: rule}
350 if rule.sameAs(pruned(ks)) && len(ks) < len(signerKeys) && subset(ks, signerKeys) && satisfiable(ks) {
351 signerKeys, signerRule = ks, rule
352 atOnce("rotate signers", text, p, made, t, "a removal the rule survives")
353 chain.Emit("SignersSet", "count", strconv.Itoa(len(ks)), "rule", rule.String())
354 return
355 }
356 p.oldKeys, p.oldRule = Signers()
357 d := queue("rotate signers", text, p, made, t)
358 d.Note += "; applies only while the signer set and rule stand as they do now"
359 })
360}
361
362// ---------------------------------------------------------------- seats --
363
364// parseSeatChanges reads "seat:address" pairs, comma separated; an empty address ("1:" or "1:-") vacates the seat.
365func parseSeatChanges(changes string) ([]int, []address) {
366 if strings.TrimSpace(changes) == "" {
367 panic("hlmailbox: no changes")
368 }
369 var ns []int
370 var as []address
371 g := 0
372 for _, p := range strings.Split(changes, ",") {
373 i := strings.Index(p, ":")
374 if i < 0 {
375 panic("hlmailbox: a change is seat:address, not " + p)
376 }
377 n, err := strconv.Atoi(strings.TrimSpace(p[:i]))
378 if err != nil || n < 1 || n > seatCount {
379 panic("hlmailbox: the seat must be 1 to 6, not " + p[:i])
380 }
381 for _, x := range ns {
382 if x == n {
383 panic("hlmailbox: seat " + strconv.Itoa(n) + " named twice")
384 }
385 }
386 a := address(strings.TrimSpace(p[i+1:]))
387 if a == "-" {
388 a = ""
389 }
390 if a != "" && !a.IsValid() {
391 panic("hlmailbox: not an address: " + string(a))
392 }
393 if a == "" && seats[n] == "" {
394 panic("hlmailbox: seat " + strconv.Itoa(n) + " is empty already")
395 }
396 if a != "" && seats[n] == a {
397 panic("hlmailbox: seat " + strconv.Itoa(n) + " holds " + string(a) + " already")
398 }
399 if a != "" && SeatOf(a) != 0 {
400 panic("hlmailbox: " + string(a) + " holds seat " + strconv.Itoa(SeatOf(a)) + " already")
401 }
402 for j, x := range as {
403 if x != "" && x == a {
404 panic("hlmailbox: " + string(a) + " named for seats " + strconv.Itoa(ns[j]) + " and " + strconv.Itoa(n))
405 }
406 }
407 if IsGuardianSeat(n) {
408 g++
409 }
410 ns = append(ns, n)
411 as = append(as, a)
412 }
413 if g > 2 {
414 panic("hlmailbox: two guardians at most per proposal")
415 }
416 return ns, as
417}
418
419func seatChangesText(ns []int, as []address) string {
420 parts := make([]string, len(ns))
421 for i, n := range ns {
422 if as[i] == "" {
423 parts[i] = "remove seat " + strconv.Itoa(n) + " (" + string(seats[n]) + ")"
424 } else {
425 parts[i] = "set seat " + strconv.Itoa(n) + " to " + string(as[i])
426 }
427 }
428 return strings.Join(parts, ", ")
429}
430
431// NewRotateSeatsRequest replaces or vacates guardian and watch-key seats: changes is "seat:address" pairs, comma
432// separated, an empty address vacating the seat; two guardians at most per proposal; a seat's current key is
433// refused. Removals alone apply at once; any replacement queues the whole proposal 7 days, and a queued one
434// applies only while the six seats still stand as they did when it was decided. A removed or replaced key's
435// flags clear with it. A removal means the key the seat holds when the proposal is made (the description names
436// it): a seat whose occupant changed by execution — a replacement landed in between — is skipped and the note says
437// so (R21), the proposal spent if nothing was left to remove.
438func NewRotateSeatsRequest(cur realm, changes string) Request {
439 mustBeLive()
440 ns, as := parseSeatChanges(changes)
441 held := make([]address, len(ns))
442 for i, n := range ns {
443 held[i] = seats[n]
444 }
445 text := "rotate seats: " + seatChangesText(ns, as)
446 return propose(cur, "rotate seats", text, func(made, t int64) {
447 mustBeLive()
448 p := rotateSeatsParams{seats: ns, addrs: as, held: held}
449 removals := true
450 for _, a := range as {
451 if a != "" {
452 removals = false
453 }
454 }
455 if removals {
456 vacated, skipped := applySeats(p, t)
457 if len(vacated) == 0 {
458 d := ledger.Record("rotate seats", text, p, made, t, t)
459 ledger.Spend(d, "spent: nothing to remove, "+seatsNote(nil, skipped))
460 chain.Emit("Decision", "id", strconv.FormatInt(d.ID, 10), "kind", "rotate seats", "state", "spent")
461 return
462 }
463 atOnce("rotate seats", text, p, made, t, "removals: "+seatsNote(vacated, skipped))
464 return
465 }
466 p.before = seats
467 d := queue("rotate seats", text, p, made, t)
468 d.Note += "; applies only while the seats stand as they do now"
469 })
470}
471
472// applySeats makes a proposal's seat changes: a replacement as named; a removal only while the seat still holds the
473// key the proposal was made against (R21). It reports the seats vacated and the removals skipped.
474func applySeats(p rotateSeatsParams, t int64) (vacated, skipped []int) {
475 for i, n := range p.seats {
476 a := p.addrs[i]
477 if a == "" {
478 if seats[n] == "" || seats[n] != p.held[i] {
479 skipped = append(skipped, n)
480 continue
481 }
482 clearSeat(n, t)
483 seats[n] = ""
484 vacated = append(vacated, n)
485 chain.Emit("SeatSet", "seat", strconv.Itoa(n), "address", "")
486 continue
487 }
488 if SeatOf(a) != 0 && SeatOf(a) != n {
489 panic("hlmailbox: " + string(a) + " holds seat " + strconv.Itoa(SeatOf(a)) + " now")
490 }
491 if seats[n] != "" {
492 clearSeat(n, t)
493 }
494 seats[n] = a
495 chain.Emit("SeatSet", "seat", strconv.Itoa(n), "address", string(a))
496 }
497 return vacated, skipped
498}
499
500// seatsNote says which seats a proposal vacated and which it skipped.
501func seatsNote(vacated, skipped []int) string {
502 var parts []string
503 if len(vacated) > 0 {
504 parts = append(parts, "seats "+seatList(vacated)+" vacated")
505 }
506 if len(skipped) > 0 {
507 parts = append(parts, "seats "+seatList(skipped)+" skipped (the occupant changed since the proposal was made)")
508 }
509 return strings.Join(parts, ", ")
510}
511
512func seatList(ns []int) string {
513 parts := make([]string, len(ns))
514 for i, n := range ns {
515 parts[i] = strconv.Itoa(n)
516 }
517 return strings.Join(parts, ",")
518}
519
520// -------------------------------------------------------------- unpause --
521
522func parseFns(fns string) []string {
523 var out []string
524 for _, p := range strings.Split(fns, ",") {
525 fn := strings.TrimSpace(p)
526 if !isFn(fn) {
527 panic("hlmailbox: the functions are sends, credits and payouts, not " + fn)
528 }
529 for _, x := range out {
530 if x == fn {
531 panic("hlmailbox: " + fn + " named twice")
532 }
533 }
534 out = append(out, fn)
535 }
536 return out
537}
538
539// NewUnpauseRequest clears every seat's flag for the named functions of a route (sends, credits, payouts; comma
540// separated), and may vacate one seat in the same stroke (removeSeat, 0 for none — one seat, so §5.3's cap of two
541// guardians per proposal holds by construction; the void builder's unpause option, which takes a list, counts
542// them). The seat's holder now is what the proposal means and is recorded with it: a seat whose occupant changed
543// by execution — a replacement landed in between — is skipped, and the note says so (R17). It applies at once;
544// the builder refuses one that would clear nothing now, and an executed one that finds nothing to clear is spent
545// (recorded, no effect, no meter reset), never queued.
546func NewUnpauseRequest(cur realm, routePath, fns string, removeSeat int) Request {
547 mustBeLive()
548 if !routes.Has(routePath) {
549 panic("hlmailbox: no route " + routePath)
550 }
551 list := parseFns(fns)
552 var removeAddr address
553 if removeSeat != 0 {
554 if removeSeat < 1 || removeSeat > seatCount {
555 panic("hlmailbox: the seat must be 1 to 6")
556 }
557 if removeAddr = seats[removeSeat]; removeAddr == "" {
558 panic("hlmailbox: seat " + strconv.Itoa(removeSeat) + " is empty")
559 }
560 }
561 clears := false
562 for _, fn := range list {
563 if Paused(routePath, fn) {
564 clears = true
565 }
566 }
567 if !clears {
568 panic("hlmailbox: no flag stands on " + fns + " of " + routePath + ": nothing to clear")
569 }
570 text := "unpause " + strings.Join(list, ",") + " on " + routePath
571 if removeSeat != 0 {
572 text += ", removing seat " + strconv.Itoa(removeSeat) + " (" + string(removeAddr) + ")"
573 }
574 return propose(cur, "unpause", text, func(made, t int64) {
575 mustBeLive()
576 p := unpauseParams{route: routePath, fns: list, removeSeat: removeSeat, removeAddr: removeAddr}
577 n, payouts := clearFns(routePath, list, t)
578 removed, skipped := false, false
579 if removeSeat != 0 {
580 if seats[removeSeat] == removeAddr {
581 clearSeat(removeSeat, t)
582 seats[removeSeat] = ""
583 removed = true
584 chain.Emit("SeatSet", "seat", strconv.Itoa(removeSeat), "address", "")
585 } else {
586 skipped = true
587 }
588 }
589 seatNote := ""
590 if removed {
591 seatNote = ", seat " + strconv.Itoa(removeSeat) + " vacated"
592 } else if skipped {
593 seatNote = ", seat " + strconv.Itoa(removeSeat) + " skipped (its occupant changed since the proposal was made)"
594 }
595 if n == 0 && !removed {
596 d := ledger.Record("unpause", text, p, made, t, t)
597 ledger.Spend(d, "spent: nothing to clear"+seatNote)
598 chain.Emit("Decision", "id", strconv.FormatInt(d.ID, 10), "kind", "unpause", "state", "spent")
599 return
600 }
601 note := strconv.Itoa(n) + " flags cleared"
602 if payouts {
603 note += ", meters restart"
604 }
605 atOnce("unpause", text, p, made, t, note+seatNote)
606 })
607}
608
609// --------------------------------------------------------------- cancel --
610
611// NewCancelRequest cancels a queued decision of this realm, at once. It is the one builder proposed before Launch.
612func NewCancelRequest(cur realm, id int64) Request {
613 d := ledger.Get(id)
614 if d == nil || !d.Pending() {
615 panic("hlmailbox: decision " + strconv.FormatInt(id, 10) + " is not pending")
616 }
617 text := "cancel decision " + strconv.FormatInt(id, 10) + " (" + d.Text + ")"
618 return propose(cur, "cancel", text, func(made, t int64) {
619 if _, err := ledger.Cancel(id, t); err != nil {
620 panic("hlmailbox: " + err.Error())
621 }
622 atOnce("cancel", text, cancelParams{id}, made, t, "cancelled")
623 })
624}
625
626// -------------------------------------------------------------- execute --
627
628// Execute applies a queued decision whose 7 days have passed. Anyone may call it; the relayers do. A queued
629// rotation of signers or seats applies only while the set it was decided against still stands: one that finds
630// the signers or the seats changed in between panics (it stays pending; GovDAO cancels it and proposes afresh).
631func Execute(cur realm, id int64) {
632 caller(cur)
633 t := now()
634 d, err := ledger.Due(id, t)
635 if err != nil {
636 panic("hlmailbox: " + err.Error())
637 }
638 note := "applied after the delay"
639 switch p := d.Params.(type) {
640 case setVerifierParams:
641 if !verifiers.Has(p.path) {
642 panic("hlmailbox: no verifier registered at " + p.path)
643 }
644 active.Set(domainKey(p.origin), p.path)
645 chain.Emit("VerifierSet", "origin", u32(p.origin), "pkgpath", p.path)
646 case rotateSignersParams:
647 if !sameKeys(signerKeys, p.oldKeys) || !signerRule.equal(p.oldRule) {
648 panic("hlmailbox: the signer set or rule changed since decision " + strconv.FormatInt(id, 10) + " was decided: cancel it and propose the rotation against the set as it stands")
649 }
650 signerKeys, signerRule = p.keys, p.rule
651 chain.Emit("SignersSet", "count", strconv.Itoa(len(p.keys)), "rule", p.rule.String())
652 case rotateSeatsParams:
653 if p.before != seats {
654 panic("hlmailbox: the seats changed since decision " + strconv.FormatInt(id, 10) + " was decided: cancel it and propose the change against the seats as they stand")
655 }
656 if _, skipped := applySeats(p, t); len(skipped) > 0 {
657 note += "; " + seatsNote(nil, skipped)
658 }
659 default:
660 panic("hlmailbox: decision " + strconv.FormatInt(id, 10) + " is not one that waits")
661 }
662 ledger.Apply(d, t, note)
663 chain.Emit("Decision", "id", strconv.FormatInt(id, 10), "kind", d.Kind, "state", "applied")
664}