Kind Realm (renderable)
Name v0
Namespace sys / validators
Exported functions 9
Module gno.land/r/sys/validators/v0
gno 0.9 cache.gno gno
⧉
1 package validators 2 3 import ( 4 "chain" 5 "chain/runtime" 6 "sort" 7 "strconv" 8 AssertGenesisValopersConsistent (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
GetValidator (addr string) struct{Address .uverse.address; PubKey string; VotingPower uint64}
GetValidators () []gno.land/p/sys/validators/v0.Validator
IsValidator (addr string) bool
NewValidatorProposalRequest (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, changes []gno.land/r/sys/validators/v0.ValoperChange, title string, description string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
NewValoperChange (operatorAddress string, power uint64) struct{OperatorAddress .uverse.address; Power uint64}
NotifyValoperChanged (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, op string, signingPubKey string, signingAddress string, keepRunning bool)
Render (string) string
RotateValoperSigningKey (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, op string, oldPubKey string, newPubKey string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.
9 "gno.land/p/nt/bptree/v0"
10 "gno.land/p/sys/validators/v0"
11 sysparams "gno.land/r/sys/params"
12 )
13
14 // valopersRealmPath is the only realm allowed to refresh valoperCache
15 // or invoke RotateValoperSigningKey. Both auth checks below depend on
16 // being called via `cross` from a crossing function in valopers.
17 const valopersRealmPath = "gno.land/r/gnops/valopers"
18
19 // valoperCache mirrors the (operator -> current signing key) view from
20 // r/gnops/valopers. Written by valopers via NotifyValoperChanged. Read
21 // by future operator-keyed proposal flow (step 5).
22 //
23 // Pushing (valopers passes the values as args) rather than pulling
24 // (v0 imports valopers and reads them) — pulling would create an
25 // import cycle, since valopers already imports v0 for IsValidator.
26 var valoperCache = bptree.NewBPTree32()
27
28 type cacheEntry struct {
29 SigningPubKey string
30 SigningAddress address
31 KeepRunning bool
32 }
33
34 // assertValopersCaller panics if the caller realm is not r/gnops/valopers.
35 // Per docs/resources/gno-interrealm.md, this check works only when (a)
36 // the host function is a crossing function (`cur realm`) and (b) it's
37 // invoked via `cross` from a crossing function in valopers. Then
38 // PreviousRealm() shifts exactly one frame to valopers. A user MsgCall
39 // would see PreviousRealm() == UserRealm (pkgpath ""); a third realm
40 // cross-call would see its own pkgpath. Either fails this check.
41 func assertValopersCaller(_ int , rlm realm) {
42 // Defense-in-depth IsCurrent gate. Current call sites pass live
43 // cur (cross(cur) from valopers), so this never fires today — but
44 // the helper trusts its rlm input, and any future call that threads
45 // a stashed/sibling-frame realm value with .Previous().PkgPath() ==
46 // valopersRealmPath would silently bypass the gate (Class-2
47 // designation forgery; see docs/resources/gno-security.md). Gating
48 // here makes the precondition enforceable rather than convention.
49 if !rlm.IsCurrent() {
50 panic( "unauthorized: rlm is not the caller's live cur" )
51 }
52 caller := rlm.Previous().PkgPath()
53 if caller != valopersRealmPath {
54 panic( "caller realm must be " + valopersRealmPath + ", got " + caller)
55 }
56 }
57
58 // NotifyValoperChanged refreshes the cached entry for op. Auth: caller
59 // realm must be r/gnops/valopers.
60 //
61 // READ-ONLY against valopers: by design this function does not call
62 // back into valopers (no pull). Valopers pushes the current values in
63 // as args. This eliminates the confused-deputy class where v0 → valopers
64 // callbacks would make valopers see v0 as PreviousRealm.
65 func NotifyValoperChanged(cur realm, op address, signingPubKey string , signingAddress address, keepRunning bool ) {
66 assertValopersCaller( 0 , cur)
67 valoperCache.Set(op.String(), cacheEntry{
68 SigningPubKey: signingPubKey,
69 SigningAddress: signingAddress,
70 KeepRunning: keepRunning,
71 })
72 }
73
74 // RotateValoperSigningKey applies a signing-key rotation to the
75 // effective valset and publishes the new full set via sysparams.
76 // Auth: caller realm must be r/gnops/valopers.
77 //
78 // Body is read-modify-write against sysparams.GetValsetEffective so
79 // concurrent same-block writers (other rotations or GovDAO executors)
80 // accumulate instead of clobbering. Mirrors the executor pattern in
81 // validators.gno.
82 //
83 // Idempotent: if the rotating operator's old signing address is not
84 // currently in the effective valset (e.g., they were removed), the
85 // rotation is a no-op at the sysparams level — valopers' profile and
86 // signingRegistry already record the new key. Either replays cleanly.
87 //
88 // Emits ValoperRotated event with op + old/new addresses + height.
89 func RotateValoperSigningKey(cur realm, op address, oldPubKey, newPubKey string ) {
90 assertValopersCaller( 0 , cur)
91
92 oldAddr, err := chain.PubKeyAddress(oldPubKey)
93 if err != nil {
94 panic( "invalid oldPubKey: " + err.Error())
95 }
96 newAddr, err := chain.PubKeyAddress(newPubKey)
97 if err != nil {
98 panic( "invalid newPubKey: " + err.Error())
99 }
100
101 baseline := sysparams.GetValsetEffective()
102 set := make( map [address]validators.Validator, len(baseline))
103 for _, v := range baseline {
104 set[v.Address] = v
105 }
106
107 // The rotating operator must currently be in the active set; if
108 // not (operator removed before rotating), nothing to publish.
109 // Valopers-side state has already been updated regardless.
110 prev, ok := set[oldAddr]
111 if !ok {
112 chain.Emit(
113 "ValoperRotated" ,
114 "op" , op.String(),
115 "oldAddr" , oldAddr.String(),
116 "newAddr" , newAddr.String(),
117 "height" , strconv.FormatInt(runtime.ChainHeight(), 10 ),
118 "applied" , "false" ,
119 )
120 return
121 }
122
123 delete(set, oldAddr)
124 set[newAddr] = validators.Validator{
125 Address: newAddr,
126 PubKey: newPubKey,
127 VotingPower: prev.VotingPower,
128 }
129
130 // Defense-in-depth: a delete+insert in this branch always leaves
131 // at least one entry (the freshly inserted newAddr), so an empty
132 // set is unreachable today. Panic explicitly anyway: a future
133 // refactor of this body that ends up publishing an empty set
134 // would otherwise be silently swallowed by the EndBlocker
135 // (which logs and clears dirty for empty publishes), masking
136 // the regression.
137 if len(set) == 0 {
138 panic( "rotation would empty the validator set; refused to keep consensus liveness" )
139 }
140
141 entries := make([] string , 0 , len(set))
142 for _, v := range set {
143 entries = append(entries, v.PubKey+ ":" +strconv.FormatUint(v.VotingPower, 10 ))
144 }
145 sort.Strings(entries)
146 sysparams.SetValsetProposal(cross(cur), entries)
147
148 chain.Emit(
149 "ValoperRotated" ,
150 "op" , op.String(),
151 "oldAddr" , oldAddr.String(),
152 "newAddr" , newAddr.String(),
153 "height" , strconv.FormatInt(runtime.ChainHeight(), 10 ),
154 "applied" , "true" ,
155 )
156 }
157
158 // AssertGenesisValopersConsistent panics if any entry in valset:current
159 // (the seeded genesis valset) lacks a corresponding valoperCache profile
160 // whose SigningAddress matches.
161 //
162 // **Genesis-mode only.** The function refuses to run unless
163 // runtime.ChainHeight() == 0. This is the documented intended usage
164 // (last migration .jsonl tx, before any block has been produced) and
165 // also closes a post-genesis MsgCall DoS surface — without the guard,
166 // an attacker could pay gas to repeatedly invoke an O(N) iteration
167 // over valoperCache + valset:current after the chain is live.
168 //
169 // gnoland's InitChainer auto-runs this assertion at end of
170 // genesis-mode replay when GnoGenesisState.PastChainIDs is non-empty;
171 // failure aborts the boot unconditionally. valoper-seed and
172 // hand-crafted migration .jsonls do NOT need to emit the call
173 // themselves.
174 //
175 // Crossing function: callable via MsgCall (only at genesis-mode).
176 // Doesn't mutate state — pure invariant check. Inverse direction
177 // (every valoperCache entry must have a corresponding valset:current
178 // entry) is intentionally NOT checked: extra valoper profiles
179 // registered without immediate valset inclusion are a normal
180 // post-genesis state.
181 func AssertGenesisValopersConsistent(cur realm) {
182 if runtime.ChainHeight() != 0 {
183 panic( "AssertGenesisValopersConsistent is only callable during genesis-mode replay (ChainHeight()==0)" )
184 }
185
186 // Collect the signing addresses present in valoperCache.
187 seen := map [ string ] bool {}
188 valoperCache.Iterate( "" , "" , func (_ string , raw any ) bool {
189 entry := raw.(cacheEntry)
190 seen[entry.SigningAddress.String()] = true
191 return false
192 })
193
194 // Every entry in valset:current must appear in seen.
195 for _, v := range sysparams.GetValsetEntries() {
196 if !seen[v.Address.String()] {
197 panic( "genesis-validator " + v.Address.String() + " has no corresponding valoper profile (signing address not in valoperCache)" )
198 }
199 }
200 }
201