decisions.gno
11.15 Kb · 293 lines
1// Package decisions is the ledger every bridge realm keeps of GovDAO's decisions (bridge/PLAN.md §5.3): a decision
2// is born when a proposal made with one of the realm's builders executes, gets the next number, and is either
3// applied at once or queued for 7 days of block time; anyone applies a queued one after its delay, and GovDAO can
4// cancel it in between. The ledger keeps no closure and no proposal text: the parameters a builder wrote are the
5// record. The realm decides what a decision does; this package keeps the numbers, the clocks and the page.
6package decisions
7
8import (
9 "errors"
10 "strconv"
11 "strings"
12
13 "gno.land/p/nt/avl/v0"
14)
15
16const (
17 // Day is a day of block time, in seconds.
18 Day = int64(86400)
19 // Factor divides every delay of the realms — Delay, Deadline, and the old router's 7 days after a swap, which
20 // is Delay (§5.9): 1 for the mainnet build, which §8.3's check reads in the served source; the devnet build
21 // is deployed with 8 here, so its 14 days pass in 42 hours, as the Solana programs' clock-factor-8 feature
22 // does. A compiled constant beside the launch values, shown on every realm's page. The meters (§5.4) are
23 // rates per real day and are not divided.
24 Factor = int64(1)
25 // Delay is the wait a raise, a swap, a credit, a migration or a sweep payment takes after its proposal
26 // executes (§5.3: 7 days by block time), divided by Factor.
27 Delay = 7 * Day / Factor
28 // Deadline is how old a proposal may be when it executes; past it the closure returns an error and GovDAO
29 // marks the proposal denied for good (§5.3: 14 days), divided by Factor.
30 Deadline = 14 * Day / Factor
31 // PageMax is how many decisions of one kind and state the page shows.
32 PageMax = 100
33 // DAOPath is the realm whose frame every builder's closure must be called from, once the governor has yielded
34 // (or from the first block, when none was compiled): the proxy that executes GovDAO's proposals.
35 DAOPath = "gno.land/r/gov/dao"
36)
37
38// ------------------------------------------------------------- governor --
39
40// The governor (M1k, Jae 2026-10-14: "until I yield to GovDAO I want to be GovDAO alone"). Each bridge realm
41// compiles a governor address beside its other launch values; while one is compiled and has not yielded, a
42// decision is the governor's own direct call through the realm's Govern — the same closure, checks, queues and
43// delays a proposal's execution runs, only who decides differs — and GovDAO's proposals are refused. The yield is
44// one call on hlmailbox (Yield), which every realm reads, so no realm is ever governed apart from the others; from
45// that block DAOPath's proxy is the only governor, as before M1k. A compiled governor of "" means GovDAO from the
46// first block. This package keeps the rule and the page line; the realms keep the identity checks, since an
47// identity is read from a live cur alone.
48
49// DAOGoverns is whether DAOPath's proxy decides now: no governor was compiled, or the governor yielded at that
50// block height (0 until the yield).
51func DAOGoverns(governor address, yielded int64) bool { return governor == "" || yielded != 0 }
52
53// GovernorLine is the line every bridge realm's page shows of who governs it.
54func GovernorLine(governor address, yielded int64) string {
55 switch {
56 case governor == "":
57 return "governor: " + DAOPath + " from the first block (no governor was compiled)"
58 case yielded == 0:
59 return "governor: " + string(governor) + " (until the yield) — it alone decides, by Govern from its own key, and " + DAOPath + "'s proposals are refused"
60 }
61 return "governor: " + DAOPath + " since height " + itoa(yielded) + " (" + string(governor) + " yielded)"
62}
63
64// GovernorJSON is the same for the agents: `"governor":"…","yielded":H` (no leading or trailing comma).
65func GovernorJSON(governor address, yielded int64) string {
66 return "\"governor\":" + strconv.Quote(string(governor)) + ",\"yielded\":" + itoa(yielded)
67}
68
69// Decision is one numbered change, as the realm's page shows it.
70type Decision struct {
71 ID int64
72 Kind string // the builder's name, e.g. "set cap floor"
73 Text string // the parameters, as the builder wrote them into the proposal's description
74 Params any // the parameters, typed, the realm's own
75 Made int64 // unix time the proposal was made (the builder call)
76 Recorded int64 // unix time the proposal executed: the decision's birth
77 Due int64 // unix time it may be applied: Recorded for an at-once change, Recorded + Delay for a queued one
78 Applied int64 // unix time it was applied; 0 while pending
79 Cancelled int64 // unix time GovDAO cancelled it; 0 if it was not
80 Spent bool // recorded with no effect (an unpause that would have cleared nothing)
81 Note string // what happened at execution, in words
82}
83
84// Pending is whether the decision still waits to be applied.
85func (d *Decision) Pending() bool { return d.Applied == 0 && d.Cancelled == 0 && !d.Spent }
86
87// Queued is whether the decision waits on its delay (pending, and not due at once).
88func (d *Decision) Queued() bool { return d.Pending() && d.Due > d.Recorded }
89
90// State is "pending", "applied", "cancelled" or "spent".
91func (d *Decision) State() string {
92 switch {
93 case d.Cancelled != 0:
94 return "cancelled"
95 case d.Spent:
96 return "spent"
97 case d.Applied != 0:
98 return "applied"
99 }
100 return "pending"
101}
102
103// Ledger numbers and keeps a realm's decisions. The zero value is empty.
104type Ledger struct {
105 seq int64
106 all avl.Tree // key(id) → *Decision
107}
108
109func key(id int64) string {
110 s := strconv.FormatInt(id, 10)
111 return strings.Repeat("0", 12-len(s)) + s
112}
113
114// Record gives a decision the next number and keeps it. due is when it may be applied (now for at once).
115func (l *Ledger) Record(kind, text string, params any, made, now, due int64) *Decision {
116 l.seq++
117 d := &Decision{ID: l.seq, Kind: kind, Text: text, Params: params, Made: made, Recorded: now, Due: due}
118 l.all.Set(key(d.ID), d)
119 return d
120}
121
122// Get is the decision with this number, or nil.
123func (l *Ledger) Get(id int64) *Decision {
124 v := l.all.Get(key(id))
125 if v == nil {
126 return nil
127 }
128 return v.(*Decision)
129}
130
131// Last is the number of the newest decision (0 if none).
132func (l *Ledger) Last() int64 { return l.seq }
133
134// Size is how many decisions were ever recorded.
135func (l *Ledger) Size() int { return l.all.Size() }
136
137// Apply marks a decision applied now, with a note.
138func (l *Ledger) Apply(d *Decision, now int64, note string) {
139 d.Applied = now
140 if note != "" {
141 d.Note = note
142 }
143}
144
145// Spend marks a decision spent: recorded, no effect.
146func (l *Ledger) Spend(d *Decision, note string) {
147 d.Spent = true
148 d.Note = note
149}
150
151// Cancel marks a pending decision cancelled now. It errors on a number that is not pending.
152func (l *Ledger) Cancel(id, now int64) (*Decision, error) {
153 d := l.Get(id)
154 if d == nil {
155 return nil, errors.New("no decision " + strconv.FormatInt(id, 10))
156 }
157 if !d.Pending() {
158 return nil, errors.New("decision " + strconv.FormatInt(id, 10) + " is " + d.State() + ", not pending")
159 }
160 d.Cancelled = now
161 return d, nil
162}
163
164// Due is a pending decision whose delay has passed, by number; it errors otherwise.
165func (l *Ledger) Due(id, now int64) (*Decision, error) {
166 d := l.Get(id)
167 if d == nil {
168 return nil, errors.New("no decision " + strconv.FormatInt(id, 10))
169 }
170 if !d.Pending() {
171 return nil, errors.New("decision " + strconv.FormatInt(id, 10) + " is " + d.State() + ", not pending")
172 }
173 if now < d.Due {
174 return nil, errors.New("decision " + strconv.FormatInt(id, 10) + " is due at " + strconv.FormatInt(d.Due, 10) + ", not before")
175 }
176 return d, nil
177}
178
179// Each walks every decision, oldest first, until fn returns true.
180func (l *Ledger) Each(fn func(d *Decision) bool) {
181 l.all.Iterate("", "", func(_ string, v any) bool { return fn(v.(*Decision)) })
182}
183
184// Expired is the one error a builder's closure returns: the proposal was made more than Deadline ago, so GovDAO
185// marks it denied for good. Everything else a closure refuses is a panic, which leaves the proposal for a retry.
186func Expired(made, now int64) error {
187 if now-made > Deadline {
188 return errors.New("the proposal is past its 14-day deadline (made at " + strconv.FormatInt(made, 10) + ", now " + strconv.FormatInt(now, 10) + ")")
189 }
190 return nil
191}
192
193// ---------------------------------------------------------------- pages --
194
195func itoa(n int64) string { return strconv.FormatInt(n, 10) }
196
197// Clocks says the clock factor and the delays it gives, for every realm's page: §8.3's check reads it against
198// the served source, where the mainnet build's factor is 1.
199func Clocks() string {
200 return "clock factor " + itoa(Factor) + " (1 on mainnet; a devnet build is compiled with 8): the 7-day delay is " + itoa(Delay) +
201 " s and the 14-day deadline " + itoa(Deadline) + " s of block time"
202}
203
204// ClocksJSON is the same for the agents: {"factor":…,"delay":…,"deadline":…}.
205func ClocksJSON() string {
206 return "{\"factor\":" + itoa(Factor) + ",\"delay\":" + itoa(Delay) + ",\"deadline\":" + itoa(Deadline) + "}"
207}
208
209// Page is the decisions page: for every kind, the pending decisions (up to PageMax) and the last PageMax executed
210// ones, newest first.
211func (l *Ledger) Page() string {
212 if l.all.Size() == 0 {
213 return "No decision yet: one exists only when a proposal made with a builder executes.\n"
214 }
215 kinds := []string{}
216 pending := map[string][]*Decision{}
217 done := map[string][]*Decision{}
218 l.all.ReverseIterate("", "", func(_ string, v any) bool {
219 d := v.(*Decision)
220 if _, ok := pending[d.Kind]; !ok {
221 kinds = append(kinds, d.Kind)
222 pending[d.Kind] = nil
223 done[d.Kind] = nil
224 }
225 if d.Pending() {
226 if len(pending[d.Kind]) < PageMax {
227 pending[d.Kind] = append(pending[d.Kind], d)
228 }
229 } else if len(done[d.Kind]) < PageMax {
230 done[d.Kind] = append(done[d.Kind], d)
231 }
232 return false
233 })
234 var b strings.Builder
235 for _, k := range kinds {
236 b.WriteString("### " + k + "\n\n")
237 if len(pending[k]) == 0 {
238 b.WriteString("pending: none\n")
239 } else {
240 b.WriteString("pending:\n")
241 for _, d := range pending[k] {
242 b.WriteString("- " + line(d) + "\n")
243 }
244 }
245 if len(done[k]) > 0 {
246 b.WriteString("\nexecuted:\n")
247 for _, d := range done[k] {
248 b.WriteString("- " + line(d) + "\n")
249 }
250 }
251 b.WriteString("\n")
252 }
253 return b.String()
254}
255
256func line(d *Decision) string {
257 s := "#" + itoa(d.ID) + " " + d.State() + ": " + d.Text + " (recorded " + itoa(d.Recorded)
258 if d.Queued() {
259 s += ", due " + itoa(d.Due)
260 }
261 switch {
262 case d.Applied != 0:
263 s += ", applied " + itoa(d.Applied)
264 case d.Cancelled != 0:
265 s += ", cancelled " + itoa(d.Cancelled)
266 }
267 s += ")"
268 if d.Note != "" {
269 s += " — " + d.Note
270 }
271 return s
272}
273
274// JSON is every decision, oldest first, as a JSON array.
275func (l *Ledger) JSON() string {
276 var b strings.Builder
277 b.WriteString("[")
278 first := true
279 l.all.Iterate("", "", func(_ string, v any) bool {
280 d := v.(*Decision)
281 if !first {
282 b.WriteString(",")
283 }
284 first = false
285 b.WriteString("{\"id\":" + itoa(d.ID) + ",\"kind\":" + strconv.Quote(d.Kind) + ",\"text\":" + strconv.Quote(d.Text) +
286 ",\"state\":" + strconv.Quote(d.State()) + ",\"made\":" + itoa(d.Made) + ",\"recorded\":" + itoa(d.Recorded) +
287 ",\"due\":" + itoa(d.Due) + ",\"applied\":" + itoa(d.Applied) + ",\"cancelled\":" + itoa(d.Cancelled) +
288 ",\"note\":" + strconv.Quote(d.Note) + "}")
289 return false
290 })
291 b.WriteString("]")
292 return b.String()
293}