limits.gno
5.97 Kb · 149 lines
1package validators
2
3import (
4 "math/overflow"
5 "strings"
6 "time"
7
8 "gno.land/r/gov/dao"
9)
10
11// Valset-update safety limits (IBC trust-level + cooldown).
12//
13// - lastValsetUpdate: timestamp of the most recent successful valset
14// proposal execution. Read on both proposal create and execute to
15// enforce the cooldown; written only after a proposal's executor
16// finishes a successful SetValsetProposal call.
17//
18// - valsetUpdateCooldown: minimum elapsed time between two consecutive
19// valset updates. The acceptance criteria in #4829 specify 24h.
20//
21// - trustLevelRatio: the fraction of the previous voting power that the
22// surviving previous validators must retain in the new set. The IBC
23// light-client spec bounds this to [1/3, 2/3] (1/3 is the BFT honest
24// floor; >2/3 would defeat the whole purpose). Update via the
25// NewTrustLevelPropRequest governance proposal.
26var (
27 lastValsetUpdate time.Time
28 valsetUpdateCooldown = 24 * time.Hour
29 trustLevelRatio = trustRatio{numerator: 1, denominator: 3}
30 trustLevelMinAllowed = trustRatio{numerator: 1, denominator: 3}
31 trustLevelMaxAllowed = trustRatio{numerator: 2, denominator: 3}
32)
33
34const (
35 errValsetUpdateCooldown = "valset update cooldown in effect"
36 errTrustLevelViolated = "trust level violated: insufficient baseline voting power retained"
37 errInvalidTrustLevel = "invalid trust level fraction"
38 errTrustLevelOverflow = "trust level arithmetic overflow"
39 errInvalidCooldown = "invalid cooldown"
40
41 // cooldownMaxSeconds caps NewCooldownPropRequest input at one year.
42 // time.Duration is int64 nanoseconds, so unbounded seconds * 1e9
43 // silently overflows past ~9.22e9 seconds (≈292 years) and could
44 // wrap to a tiny or negative duration — effectively disabling the
45 // cooldown by accident. A 1-year ceiling is far beyond any
46 // reasonable governance choice and well inside int64 range.
47 cooldownMaxSeconds uint64 = 365 * 24 * 60 * 60
48)
49
50// trustRatio represents num/den. Comparisons are done by cross-multiplication
51// to stay deterministic across architectures (no floats).
52type trustRatio struct {
53 numerator uint64
54 denominator uint64
55}
56
57// lessThan reports whether tr < other. Cross-multiply: a/b < c/d iff a*d < c*b.
58// Uses overflow-checked multiplication; an overflow on either side panics
59// rather than returning a silently-wrong comparison. Denominators are
60// non-zero by construction (NewTrustLevelPropRequest rejects denominator==0
61// before calling this; trustLevelMin/Max have denominator=3).
62func (tr trustRatio) lessThan(other trustRatio) bool {
63 left, okL := overflow.Mulu64(tr.numerator, other.denominator)
64 right, okR := overflow.Mulu64(other.numerator, tr.denominator)
65 if !okL || !okR {
66 panic(errTrustLevelOverflow)
67 }
68 return left < right
69}
70
71// GetTrustLevel returns the currently configured trust level as (numerator, denominator).
72func GetTrustLevel() (uint64, uint64) {
73 return trustLevelRatio.numerator, trustLevelRatio.denominator
74}
75
76// NewTrustLevelPropRequest builds a GovDAO proposal that, when executed,
77// updates trustLevelRatio. The ratio must be in [1/3, 2/3] both at
78// creation and execution time (re-checked in the callback in case bounds
79// change between propose and execute).
80func NewTrustLevelPropRequest(numerator, denominator uint64, title, description string) dao.ProposalRequest {
81 title = strings.TrimSpace(title)
82 if title == "" {
83 panic("proposal title is empty")
84 }
85 if denominator == 0 {
86 panic(errInvalidTrustLevel)
87 }
88 newRatio := trustRatio{numerator: numerator, denominator: denominator}
89 if newRatio.lessThan(trustLevelMinAllowed) || trustLevelMaxAllowed.lessThan(newRatio) {
90 panic(errInvalidTrustLevel)
91 }
92
93 return dao.NewProposalRequest(title, description, newTrustLevelExecutor(newRatio))
94}
95
96// newTrustLevelExecutor builds the GovDAO executor that applies a
97// trust-level update. The bounds are re-checked at execute-time in case
98// trustLevelMinAllowed / trustLevelMaxAllowed changed between propose
99// and execute (today they're constants, but the executor stays defensive).
100func newTrustLevelExecutor(newRatio trustRatio) dao.Executor {
101 callback := func(cur realm) error {
102 if newRatio.lessThan(trustLevelMinAllowed) || trustLevelMaxAllowed.lessThan(newRatio) {
103 panic(errInvalidTrustLevel)
104 }
105 trustLevelRatio = newRatio
106 return nil
107 }
108 return dao.NewSimpleExecutor(callback, "")
109}
110
111// GetCooldown returns the currently configured valset-update cooldown,
112// expressed in seconds. The default is 24h (per the #4829 acceptance
113// criteria); governance can tune it via NewCooldownPropRequest.
114func GetCooldown() uint64 {
115 return uint64(valsetUpdateCooldown / time.Second)
116}
117
118// NewCooldownPropRequest builds a GovDAO proposal that, when executed,
119// updates valsetUpdateCooldown. The new duration is given in seconds;
120// 0 disables the cooldown entirely (useful for testnets and
121// integration tests). The executor reads the cooldown live (not via
122// snapshot) so a successful proposal takes effect immediately for any
123// valset proposal created after it executes. In-flight valset
124// proposals that passed their own create-time cooldown check are
125// re-checked at execute time against the (now possibly different)
126// live cooldown — this is intentional so governance can both shorten
127// AND lengthen the cooldown without surprises.
128func NewCooldownPropRequest(seconds uint64, title, description string) dao.ProposalRequest {
129 title = strings.TrimSpace(title)
130 if title == "" {
131 panic("proposal title is empty")
132 }
133 if seconds > cooldownMaxSeconds {
134 panic(errInvalidCooldown)
135 }
136 newCooldown := time.Duration(seconds) * time.Second
137 return dao.NewProposalRequest(title, description, newCooldownExecutor(newCooldown))
138}
139
140// newCooldownExecutor builds the GovDAO executor that applies a
141// cooldown update. Split out for direct testability (ProposalRequest
142// doesn't expose its captured executor).
143func newCooldownExecutor(newCooldown time.Duration) dao.Executor {
144 callback := func(cur realm) error {
145 valsetUpdateCooldown = newCooldown
146 return nil
147 }
148 return dao.NewSimpleExecutor(callback, "")
149}