policy.gno
4.23 Kb · 161 lines
1package memba_gov
2
3import "time"
4
5// Classes. A target hard-codes the minimum class of each action it governs.
6const (
7 Routine = 1
8 Financial = 2
9 Critical = 3
10)
11
12// A route is one way a proposal passes: a rule on the YES votes of the
13// seated roster and the delay that follows. Thresholds are fractions of the
14// seated weight W and persons N. Every rule needs at least two persons: the
15// critical ones because N is at least 2.
16type route struct {
17 rule int
18 delay time.Duration
19}
20
21const (
22 ruleRoutine = iota
23 ruleFinancial
24 ruleWeighted
25 ruleHeadcount
26)
27
28func meets(rule, w, p, W, N int) bool {
29 switch rule {
30 case ruleRoutine: // >= 3/8 of the weight
31 return 8*w >= 3*W && p >= 2
32 case ruleFinancial: // >= 3/5 of the weight and a majority of persons
33 return 5*w >= 3*W && p >= 3 && 2*p > N
34 case ruleWeighted: // >= 2/3 of the weight and a majority of persons
35 return 3*w >= 2*W && 2*p > N
36 case ruleHeadcount: // >= 2/3 of the persons and a majority of the weight
37 return 3*p >= 2*N && 2*w > W
38 }
39 return false
40}
41
42// routes returns the ways proposal p passes: its class sets the rules, and
43// a roster action of this realm has its own delay on every route.
44func routes(p *proposal) []route {
45 var rs []route
46 switch p.class {
47 case Routine:
48 rs = []route{{ruleRoutine, 0}}
49 case Financial:
50 rs = []route{{ruleFinancial, 0}}
51 default:
52 rs = []route{{ruleWeighted, WeightedDelay}, {ruleHeadcount, HeadcountDelay}}
53 }
54 if p.target == corePath {
55 for i := range rs {
56 rs[i].delay = rosterDelay(p.action)
57 }
58 }
59 return rs
60}
61
62// A streak is one current YES ballot: when it began and its voter's weight.
63type streak struct {
64 since int64
65 weight int
66}
67
68// yesStreaks returns p's current YES ballots, oldest streak first.
69func yesStreaks(p *proposal) []streak {
70 var ys []streak
71 for _, b := range p.ballots {
72 if b.vote != voteYes {
73 continue
74 }
75 x := streak{b.since, weightOf(b.person)}
76 ys = append(ys, x)
77 i := len(ys) - 1
78 for ; i > 0 && ys[i-1].since > x.since; i-- {
79 ys[i] = ys[i-1]
80 }
81 ys[i] = x
82 }
83 return ys
84}
85
86// qualifiedSince returns when (unix seconds) the earliest coalition still
87// voting YES without interruption first met rule, or 0. The coalition of a
88// start time is every YES streak begun no later; every rule only grows with
89// more votes, so the first prefix of ys that meets it ends on that time
90// (streaks begun in the same second share it). A voter the rest of the
91// coalition does not need cannot postpone the clock by withdrawing and
92// rejoining.
93func qualifiedSince(ys []streak, rule, W, N int) int64 {
94 w, n := 0, 0
95 for _, x := range ys {
96 w += x.weight
97 n++
98 if meets(rule, w, n, W, N) {
99 return x.since
100 }
101 }
102 return 0
103}
104
105// Proposal states, in the order a reader checks them.
106const (
107 stExecuted = "executed"
108 stInvalidated = "invalidated"
109 stExpired = "expired"
110 stReady = "ready"
111 stTimelocked = "timelocked"
112 stVoting = "voting"
113)
114
115// state returns p's status and, once some route qualified, the earliest
116// unix time it is or becomes executable (0 before). Voting ends just before
117// the deadline second; readyAt and the end of the execution window are
118// inclusive.
119func state(p *proposal) (string, int64) {
120 if p.consumed {
121 return stExecuted, 0
122 }
123 if p.globalGen != globalGen || p.scopeGen != scopeGen(p.target, p.scope) || vetoed(p) {
124 return stInvalidated, 0
125 }
126 t := now()
127 W, N := seatedTotals()
128 ys := yesStreaks(p)
129 var readyAt, longest int64
130 for _, r := range routes(p) {
131 if d := secs(r.delay); d > longest {
132 longest = d
133 }
134 if since := qualifiedSince(ys, r.rule, W, N); since != 0 {
135 if at := since + secs(r.delay); readyAt == 0 || at < readyAt {
136 readyAt = at
137 }
138 }
139 }
140 deadline := p.created + secs(VotingPeriod)
141 if t > deadline+longest+secs(ExecutionWindow) || (readyAt == 0 && t >= deadline) {
142 return stExpired, readyAt
143 }
144 switch {
145 case readyAt == 0:
146 return stVoting, readyAt
147 case t < readyAt:
148 return stTimelocked, readyAt
149 }
150 return stReady, readyAt
151}
152
153// vetoed reports whether the member a RemoveInactive names has been active
154// since the proposal: proposing, voting, executing or joining vetoes it.
155func vetoed(p *proposal) bool {
156 if p.target != corePath || p.action != actRemoveInactive {
157 return false
158 }
159 m := personByID(p.subject)
160 return m != nil && m.lastActive >= p.created
161}