Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}