gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/governor/v0
PackageOpen in gnoweb ↗- Kind
- Pure package
- Name
- v0
- Namespace
- g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6 / governor
- Exported functions
- n/a — not supported for pure packages by the node (vm/qfuncs)
- Module
- gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/governor/v0
- gno
- 0.9
rules.gnogno
1package governor23import (4 "strconv"5 "strings"67 ufmt "gno.land/p/nt/ufmt/v0"8
not supported for pure packages by the node (vm/qfuncs)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
)
9
10// rulesKind changes what it takes to pass a kind.
11//
12// govern:rules "<kind> quorum=2000 threshold=6600 voting=120960 delay=34560 grace=241920 propose=100"
13//
14// All six terms. ProposeBps was once missing, and it is where a bad value is
15// worst: above a hundred percent nobody can open a question under that kind at
16// all — adopted, unusable, and unfixable by the one kind that changes terms.
17//
18// Fields left out keep their value, so lengthening one delay is a short line.
19//
20// Space-separated key=value because a governance payload has to be readable
21// where it is read — a URL, a wallet's confirmation screen, a forum post. JSON
22// puts a parser between a voter and their intent.
23type rulesKind struct{}
24
25func (k rulesKind) Name() string { return reserved + "rules" }
26
27// Describe renders the requested change and NOTHING about the world.
28//
29// Deliberately not "from 20% to 30%": that would read live state, so two
30// voters at different heights would see different questions at the same URL,
31// and a proposal that lands in between would silently rewrite what everyone
32// else is voting on.
33func (k rulesKind) describe(g *Governor, payload string) string {
34 name, set, err := parseRules(payload)
35 if err != nil {
36 return "malformed rules change: " + err.Error()
37 }
38 out := "change the rules for `" + name + "`:"
39 for _, f := range set {
40 out += ufmt.Sprintf("\n- %s -> %s", f.key, f.human())
41 }
42 return out
43}
44
45func (k rulesKind) check(g *Governor, payload string) error {
46 name, set, err := parseRules(payload)
47 if err != nil {
48 return err
49 }
50 e := g.entryOf(name)
51 if e == nil {
52 return errNoSuchKind
53 }
54 if !e.live {
55 // Retuning something not yet adopted would be the same swap the
56 // immutable-name rule closes, moved from the code to the terms: change
57 // a kind's quorum while its adoption vote is open and holders approve
58 // one set of terms and get another. What is not adopted is not the
59 // governor's to tune.
60 return errNotLive
61 }
62 // No check for an empty change set. parseRules refuses a payload with
63 // fewer than two fields, and every field after the first either parses or
64 // returns an error, so a nil error here guarantees at least one change. A
65 // guard would be unreachable, and unreachable defence reads as a case
66 // somebody thought could happen.
67 next := e.rules
68 applyRules(&next, set)
69 // The same sanity the offering path enforces, and literally the same code:
70 // one function, two callers. A second copy is how the two paths come to
71 // disagree about what a proposer must hold.
72 return saneRules(next)
73}
74
75func (k rulesKind) run(g *Governor, dispatch Dispatch, payload string) error {
76 name, set, err := parseRules(payload)
77 if err != nil {
78 return err
79 }
80 e := g.entryOf(name)
81 if e == nil {
82 return errNoSuchKind
83 }
84 next := e.rules
85 applyRules(&next, set)
86 // Only the entry changes. Proposals already open kept a COPY of the rules
87 // they were opened under, so this cannot move the bar under a vote that is
88 // already being cast.
89 e.rules = next
90 return nil
91}
92
93type field struct {
94 key string
95 n int64
96}
97
98func (f field) human() string {
99 switch f.key {
100 case "quorum", "threshold", "propose":
101 // A percentage as well as the raw figure: basis points are exact and
102 // nobody reads them. Padded by hand because ufmt has no zero-padding
103 // and its width digits apply to %s alone, so %02d prints 1 rather than
104 // 01 and one basis point renders as 0.1% — ten times the truth.
105 return ufmt.Sprintf("%d bps (%s)", f.n, pct(f.n))
106 default:
107 // The same rendering the adoption page uses. It printed a bare "%d
108 // blocks" while adoptKind printed a duration, so the same field read
109 // two different ways depending on which page you arrived at.
110 return span(f.n)
111 }
112}
113
114func parseRules(payload string) (string, []field, error) {
115 parts := strings.Fields(payload)
116 if len(parts) < 2 {
117 return "", nil, govErr("expected a kind name and at least one key=value")
118 }
119 name := parts[0]
120 var set []field
121 for _, p := range parts[1:] {
122 eq := strings.Index(p, "=")
123 if eq <= 0 || eq == len(p)-1 {
124 return "", nil, govErr("expected key=value, got " + p)
125 }
126 key, raw := p[:eq], p[eq+1:]
127 switch key {
128 case "quorum", "threshold", "voting", "delay", "grace", "propose":
129 default:
130 return "", nil, govErr("unknown field " + key)
131 }
132 n, err := strconv.ParseInt(raw, 10, 64)
133 if err != nil || n < 0 {
134 return "", nil, govErr("expected a non-negative number for " + key)
135 }
136 set = append(set, field{key: key, n: n})
137 }
138 return name, set, nil
139}
140
141// applyRules applies an already-validated change set to r. It cannot fail:
142// parseRules has validated every key and value, so a default/error path here would
143// be unreachable (unreachable defence reads as a case somebody thought could happen).
144func applyRules(r *Rules, set []field) {
145 for _, f := range set {
146 switch f.key {
147 case "quorum":
148 r.QuorumBps = f.n
149 case "threshold":
150 r.ThresholdBps = f.n
151 case "voting":
152 r.VotingBlocks = f.n
153 case "delay":
154 r.DelayBlocks = f.n
155 case "grace":
156 r.GraceBlocks = f.n
157 case "propose":
158 r.ProposeBps = f.n
159 }
160 }
161}
162
163// pct renders basis points as a percentage.
164//
165// Padded by hand because ufmt has no zero-padding and its width digits apply
166// to %s alone, so %02d prints 1 rather than 01 and one basis point reads as
167// 0.1% — ten times the truth, on the line a voter actually reads.
168func pct(n int64) string {
169 // n/(bps/100) is the percentage; bps/100 is 100, so the split is n/100 and
170 // n%100. Derived from the constant rather than written as 100, so a change
171 // to the scale cannot leave this rendering the old one.
172 per := bps / 100
173 frac := strconv.FormatInt(n%per, 10)
174 for int64(len(frac)) < 2 {
175 frac = "0" + frac
176 }
177 return ufmt.Sprintf("%d.%s%%", n/per, frac)
178}
179