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.

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}