valset.gno
5.00 Kb · 138 lines
1package params
2
3import (
4 "chain"
5 "errors"
6 "strconv"
7 "strings"
8
9 prms "sys/params"
10
11 "gno.land/p/sys/validators"
12)
13
14// Param keys read by gno.land/pkg/gnoland (EndBlocker).
15// Keep in sync with gno.land/pkg/gnoland/node_params.go.
16// nodeModulePrefix is declared in halt.gno (same package).
17const (
18 valsetSubmodule = "valset"
19
20 // dirty signals the chain that the proposed valset differs from the
21 // current applied one. Realm sets true; EndBlocker clears.
22 valsetDirtyKey = "dirty"
23
24 // One []string per slot; each entry has the form "<pubkey>:<power>"
25 // (bech32 pubkey + decimal power). Address is derived from pubkey
26 // on the chain side and not stored.
27 //
28 // proposed = v3's full target valset
29 // current = chain-managed: the set that becomes ACTIVE AT H+2
30 // once the most recent EndBlock's updates apply.
31 // NOT necessarily the set actively signing the current
32 // block — see ABCI H+2 sequencing.
33 valsetProposedKey = "proposed"
34 valsetCurrentKey = "current"
35
36 // Only this realm may write valset:proposed and valset:dirty.
37 // valset:current is chain-managed (see ctx-sentinel in node_params.go).
38 valsetAuthorizedRealm = "gno.land/r/sys/validators/v3"
39)
40
41// SetValsetProposal publishes the realm's desired valset. Each entry is
42// "<bech32-pubkey>:<decimal-power>"; power=0 removes the validator.
43// The chain reads this on the next EndBlocker, diffs it against
44// valset:current, and propagates the changes to consensus.
45func SetValsetProposal(cur realm, entries []string) {
46 assertValsetCaller(0, cur)
47 prms.SetSysParamStrings(nodeModulePrefix, valsetSubmodule, valsetProposedKey, entries)
48 prms.SetSysParamBool(nodeModulePrefix, valsetSubmodule, valsetDirtyKey, true)
49}
50
51// GetValsetEntries returns the chain's authoritative committed
52// validator set (the contents of valset:current). This is the
53// V_{H+2} view — the set that will be active at H+2 once the most
54// recent EndBlock's updates apply, NOT the set signing the current
55// block. Callers that want "what v3 reports as the current
56// validator set" — including the in-flight proposed set during
57// the dirty window — should call GetValsetEffective instead.
58func GetValsetEntries() []validators.Validator {
59 return parseValsetSlot(valsetCurrentKey)
60}
61
62// ValsetDirty reports whether valset:proposed is awaiting EndBlocker.
63// Realm callers MUST treat this as transient: the dirty flag is set
64// by SetValsetProposal and cleared by the chain's EndBlocker (every
65// block where dirty=true on entry exits with dirty=false).
66func ValsetDirty() bool {
67 d, _ := prms.GetSysParamBool(nodeModulePrefix, valsetSubmodule, valsetDirtyKey)
68 return d
69}
70
71// GetValsetEffective returns the set that WILL be active at H+2:
72// valset:proposed if dirty, else valset:current. Used by v3 so that
73// (a) reads after a same-block proposal callback see that proposal's
74// effects, and (b) sequential same-block proposals accumulate
75// correctly on top of each other.
76//
77// Misuse warning: this exists for r/sys/validators/v3's internal
78// reads. Other realms making "is X a validator" decisions should
79// call v3.IsValidator, not this directly, so future changes to v3's
80// read semantics propagate uniformly.
81func GetValsetEffective() []validators.Validator {
82 key := valsetCurrentKey
83 if ValsetDirty() {
84 key = valsetProposedKey
85 }
86 return parseValsetSlot(key)
87}
88
89func parseValsetSlot(key string) []validators.Validator {
90 raw, _ := prms.GetSysParamStrings(nodeModulePrefix, valsetSubmodule, key)
91 out := make([]validators.Validator, 0, len(raw))
92 for _, e := range raw {
93 v, err := parseEntry(e)
94 if err != nil {
95 panic("valset:" + key + " corrupted: " + err.Error())
96 }
97 out = append(out, v)
98 }
99 return out
100}
101
102// parseEntry splits "<bech32-pubkey>:<decimal-power>" and derives the
103// validator address via the chain.PubKeyAddress native helper.
104func parseEntry(entry string) (validators.Validator, error) {
105 pkStr, pStr, ok := strings.Cut(entry, ":")
106 if !ok {
107 return validators.Validator{}, errors.New("missing ':' separator in " + entry)
108 }
109 addr, err := chain.PubKeyAddress(pkStr)
110 if err != nil {
111 return validators.Validator{}, err
112 }
113 power, err := strconv.ParseUint(pStr, 10, 64)
114 if err != nil {
115 return validators.Validator{}, err
116 }
117 return validators.Validator{
118 Address: addr,
119 PubKey: pkStr,
120 VotingPower: power,
121 }, nil
122}
123
124func assertValsetCaller(_ int, rlm realm) {
125 // Defense-in-depth IsCurrent gate. Current call sites all pass live
126 // cur (cross(cur) from cache.gno / proposal.gno), so this never
127 // fires today — but the helper trusts its rlm input, and a future
128 // caller that threads a stashed/sibling-frame realm value would
129 // silently bypass the PkgPath check below (Class-2 designation
130 // forgery; see docs/resources/gno-security.md). Gating here makes
131 // the precondition enforceable rather than convention.
132 if !rlm.IsCurrent() {
133 panic("unauthorized: rlm is not the caller's live cur")
134 }
135 if rlm.Previous().PkgPath() != valsetAuthorizedRealm {
136 panic("unauthorized: only " + valsetAuthorizedRealm + " may write valset params")
137 }
138}