valset.gno
5.39 Kb · 147 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 // pubkey_types: chain-mirrored validator pubkey-type allow-list (read-only here).
37 valsetPubKeyTypesKey = "pubkey_types"
38
39 // Only this realm may write valset:proposed and valset:dirty.
40 // valset:current is chain-managed (see ctx-sentinel in node_params.go).
41 valsetAuthorizedRealm = "gno.land/r/sys/validators/v3"
42)
43
44// SetValsetProposal publishes the realm's desired valset. Each entry is
45// "<bech32-pubkey>:<decimal-power>"; power=0 removes the validator.
46// The chain reads this on the next EndBlocker, diffs it against
47// valset:current, and propagates the changes to consensus.
48func SetValsetProposal(cur realm, entries []string) {
49 assertValsetCaller(0, cur)
50 prms.SetSysParamStrings(nodeModulePrefix, valsetSubmodule, valsetProposedKey, entries)
51 prms.SetSysParamBool(nodeModulePrefix, valsetSubmodule, valsetDirtyKey, true)
52}
53
54// GetValsetEntries returns the chain's authoritative committed
55// validator set (the contents of valset:current). This is the
56// V_{H+2} view — the set that will be active at H+2 once the most
57// recent EndBlock's updates apply, NOT the set signing the current
58// block. Callers that want "what v3 reports as the current
59// validator set" — including the in-flight proposed set during
60// the dirty window — should call GetValsetEffective instead.
61func GetValsetEntries() []validators.Validator {
62 return parseValsetSlot(valsetCurrentKey)
63}
64
65// ValsetDirty reports whether valset:proposed is awaiting EndBlocker.
66// Realm callers MUST treat this as transient: the dirty flag is set
67// by SetValsetProposal and cleared by the chain's EndBlocker (every
68// block where dirty=true on entry exits with dirty=false).
69func ValsetDirty() bool {
70 d, _ := prms.GetSysParamBool(nodeModulePrefix, valsetSubmodule, valsetDirtyKey)
71 return d
72}
73
74// GetValsetEffective returns the set that WILL be active at H+2:
75// valset:proposed if dirty, else valset:current. Used by v3 so that
76// (a) reads after a same-block proposal callback see that proposal's
77// effects, and (b) sequential same-block proposals accumulate
78// correctly on top of each other.
79//
80// Misuse warning: this exists for r/sys/validators/v3's internal
81// reads. Other realms making "is X a validator" decisions should
82// call v3.IsValidator, not this directly, so future changes to v3's
83// read semantics propagate uniformly.
84func GetValsetEffective() []validators.Validator {
85 key := valsetCurrentKey
86 if ValsetDirty() {
87 key = valsetProposedKey
88 }
89 return parseValsetSlot(key)
90}
91
92func parseValsetSlot(key string) []validators.Validator {
93 raw, _ := prms.GetSysParamStrings(nodeModulePrefix, valsetSubmodule, key)
94 out := make([]validators.Validator, 0, len(raw))
95 for _, e := range raw {
96 v, err := parseEntry(e)
97 if err != nil {
98 panic("valset:" + key + " corrupted: " + err.Error())
99 }
100 out = append(out, v)
101 }
102 return out
103}
104
105// parseEntry splits "<bech32-pubkey>:<decimal-power>" and derives the
106// validator address via the chain.PubKeyAddress native helper.
107func parseEntry(entry string) (validators.Validator, error) {
108 pkStr, pStr, ok := strings.Cut(entry, ":")
109 if !ok {
110 return validators.Validator{}, errors.New("missing ':' separator in " + entry)
111 }
112 addr, err := chain.PubKeyAddress(pkStr)
113 if err != nil {
114 return validators.Validator{}, err
115 }
116 power, err := strconv.ParseUint(pStr, 10, 64)
117 if err != nil {
118 return validators.Validator{}, err
119 }
120 return validators.Validator{
121 Address: addr,
122 PubKey: pkStr,
123 VotingPower: power,
124 }, nil
125}
126
127// GetValsetPubKeyTypes returns the validator pubkey-type allow-list mirrored from consensus params (empty means accept any).
128func GetValsetPubKeyTypes() []string {
129 types, _ := prms.GetSysParamStrings(nodeModulePrefix, valsetSubmodule, valsetPubKeyTypesKey)
130 return types
131}
132
133func assertValsetCaller(_ int, rlm realm) {
134 // Defense-in-depth IsCurrent gate. Current call sites all pass live
135 // cur (cross(cur) from cache.gno / proposal.gno), so this never
136 // fires today — but the helper trusts its rlm input, and a future
137 // caller that threads a stashed/sibling-frame realm value would
138 // silently bypass the PkgPath check below (Class-2 designation
139 // forgery; see docs/resources/gno-security.md). Gating here makes
140 // the precondition enforceable rather than convention.
141 if !rlm.IsCurrent() {
142 panic("unauthorized: rlm is not the caller's live cur")
143 }
144 if rlm.Previous().PkgPath() != valsetAuthorizedRealm {
145 panic("unauthorized: only " + valsetAuthorizedRealm + " may write valset params")
146 }
147}