Kind Realm (renderable)
Name v0
Namespace sys / validators
Exported functions 9
Module gno.land/r/sys/validators/v0
gno 0.9 proposal_test.gno gno
⧉
1 package validators 2 3 import ( 4 "strconv" 5 "testing" 6 7 "gno.land/p/nt/testutils/v0" 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.
"gno.land/p/nt/uassert/v0"
9 "gno.land/p/nt/urequire/v0"
10 sysparams "gno.land/r/sys/params"
11 )
12
13 // seedCache populates valoperCache with the given (op, pubkey, kr)
14 // tuples — used in tests to satisfy NewValidatorProposalRequest's
15 // creation-time membership check without going through valopers.
16 // asGovDAOProxy is the realm these tests stand in as before invoking an
17 // executor directly. SimpleExecutor.Execute is invocable only from the
18 // r/gov/dao namespace; on the live route dao.ExecuteProposal and
19 // impl.ExecuteProposal thread the proxy's cur to it non-crossing, so
20 // Previous() inside Execute is this path. Calling Execute from the test's
21 // own realm is a route no transaction takes.
22 const asGovDAOProxy = "gno.land/r/gov/dao"
23
24 func seedCache(t *testing.T, entries [] struct {
25 op address
26 pubKey string
27 keepRunning bool
28 }) {
29 t.Helper()
30 for _, e := range entries {
31 signingAddr := mustAddr(t, e.pubKey)
32 valoperCache.Set(e.op.String(), cacheEntry{
33 SigningPubKey: e.pubKey,
34 SigningAddress: signingAddr,
35 KeepRunning: e.keepRunning,
36 })
37 }
38 }
39
40 func TestNewValidatorProposalRequest_RejectsUnknownOperator(cur realm, t *testing.T) {
41 resetCache()
42
43 op := testutils.TestAddress( "ghost-op" )
44
45 uassert.PanicsContains(t, cur, "unknown operator" , func () {
46 _ = NewValidatorProposalRequest(cur,
47 []ValoperChange{{OperatorAddress: op, Power: 1 }},
48 "add ghost" ,
49 "" ,
50 )
51 })
52 }
53
54 func TestNewValidatorProposalRequest_RejectsEmptyChanges(cur realm, t *testing.T) {
55 resetCache()
56
57 uassert.PanicsContains(t, cur, errNoValoperChanges, func () {
58 _ = NewValidatorProposalRequest(cur, nil , "title" , "" )
59 })
60 }
61
62 func TestNewValidatorProposalRequest_RejectsEmptyTitle(cur realm, t *testing.T) {
63 resetCache()
64 op := testutils.TestAddress( "op-A" )
65 seedCache(t, [] struct {
66 op address
67 pubKey string
68 keepRunning bool
69 }{{op: op, pubKey: pubKeyA, keepRunning: true }})
70
71 uassert.PanicsContains(t, cur, "proposal title is empty" , func () {
72 _ = NewValidatorProposalRequest(cur,
73 []ValoperChange{{OperatorAddress: op, Power: 1 }},
74 " " ,
75 "" ,
76 )
77 })
78 }
79
80 func TestNewValidatorProposalRequest_RejectsTooManyChanges(cur realm, t *testing.T) {
81 resetCache()
82
83 // Seed 41 cache entries so the membership check passes; the
84 // length cap should fire before the per-entry validation.
85 changes := make([]ValoperChange, 41 )
86 pubkeys := [] string {pubKeyA, pubKeyB, pubKeyC}
87 for i := 0 ; i < 41 ; i++ {
88 op := testutils.TestAddress( "op-" + strconv.Itoa(i))
89 pk := pubkeys[i% 3 ]
90 valoperCache.Set(op.String(), cacheEntry{
91 SigningPubKey: pk,
92 SigningAddress: mustAddr(t, pk),
93 KeepRunning: true ,
94 })
95 changes[i] = ValoperChange{OperatorAddress: op, Power: 1 }
96 }
97
98 uassert.PanicsContains(t, cur, "max number of allowed validators per proposal is 40" , func () {
99 _ = NewValidatorProposalRequest(cur, changes, "too many" , "" )
100 })
101 }
102
103 func TestNewValidatorProposalRequest_DescriptionRendering(cur realm, t *testing.T) {
104 resetCache()
105 opA := testutils.TestAddress( "op-A" )
106 opB := testutils.TestAddress( "op-B" )
107 seedCache(t, [] struct {
108 op address
109 pubKey string
110 keepRunning bool
111 }{
112 {op: opA, pubKey: pubKeyA, keepRunning: true },
113 {op: opB, pubKey: pubKeyB, keepRunning: true },
114 })
115
116 pr := NewValidatorProposalRequest(cur,
117 []ValoperChange{
118 {OperatorAddress: opA, Power: 5 },
119 {OperatorAddress: opB, Power: 0 },
120 },
121 "mixed changes" ,
122 "context line" ,
123 )
124
125 desc := pr.Description()
126 urequire.True(t, len(desc) > 0 )
127 uassert.True(t, contains(desc, "context line" ))
128 uassert.True(t, contains(desc, "## Validator Updates" ))
129 uassert.True(t, contains(desc, opA.String()+ ": add (power 5)" ))
130 uassert.True(t, contains(desc, opB.String()+ ": remove" ))
131 }
132
133 func TestNewValidatorProposalRequest_ExecutorReResolvesPubkey(cur realm, t *testing.T) {
134 // Creation-time captured changes: ValoperChange refers to opA.
135 // Cache for opA points to pubKeyA at creation. Before execution,
136 // opA's cache entry is updated to pubKeyB. Executor must publish
137 // the NEW pubkey, not the creation-time one.
138 resetValset(t)
139 resetCache()
140
141 opA := testutils.TestAddress( "op-A" )
142 seedCache(t, [] struct {
143 op address
144 pubKey string
145 keepRunning bool
146 }{{op: opA, pubKey: pubKeyA, keepRunning: true }})
147
148 changes := []ValoperChange{{OperatorAddress: opA, Power: 7 }}
149
150 // Build the executor; it captures `changes` by reference (slice
151 // of structs) but resolves SigningPubKey at run-time via cache.
152 exec := newValoperChangeExecutor(cur, changes)
153
154 // Simulate operator rotation: opA's cache entry now points to
155 // pubKeyB. Captured changes slice is unchanged.
156 valoperCache.Set(opA.String(), cacheEntry{
157 SigningPubKey: pubKeyB,
158 SigningAddress: mustAddr(t, pubKeyB),
159 KeepRunning: true ,
160 })
161
162 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
163
164 urequire.NoError(t, exec.Execute(cross(cur)))
165
166 // Effective valset should contain pubKeyB (post-rotation), not
167 // pubKeyA (creation-time).
168 effective := sysparams.GetValsetEffective()
169 urequire.Equal(t, 1 , len(effective))
170 uassert.Equal(t, pubKeyB, effective[ 0 ].PubKey)
171 uassert.Equal(t, uint64 ( 7 ), effective[ 0 ].VotingPower)
172 }
173
174 func TestNewValidatorProposalRequest_RemoveOperator(cur realm, t *testing.T) {
175 resetValset(t)
176 resetCache()
177
178 // Seed valset with opA already signing under pubKeyA.
179 testing.SetSysParamStrings(module, submodule, currKey, [] string {pubKeyA + ":10" })
180
181 opA := testutils.TestAddress( "op-A" )
182 seedCache(t, [] struct {
183 op address
184 pubKey string
185 keepRunning bool
186 }{{op: opA, pubKey: pubKeyA, keepRunning: false }})
187
188 // Liveness floor: removing the only validator empties the set.
189 // Executor runs inside a crossing dao.Executor.Execute call, so
190 // the panic surfaces as an abort, not a regular panic.
191 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
192
193 uassert.AbortsContains(t, cur, "would empty the validator set" , func () {
194 _ = newValoperChangeExecutor(cur, []ValoperChange{{OperatorAddress: opA, Power: 0 }}).Execute(cross(cur))
195 })
196 }
197
198 func TestNewValidatorProposalRequest_RemoveLeavesOthers(cur realm, t *testing.T) {
199 resetValset(t)
200 resetCache()
201
202 // Seed valset with two validators.
203 testing.SetSysParamStrings(module, submodule, currKey, [] string {
204 pubKeyA + ":10" ,
205 pubKeyB + ":5" ,
206 })
207
208 opA := testutils.TestAddress( "op-A" )
209 opB := testutils.TestAddress( "op-B" )
210 seedCache(t, [] struct {
211 op address
212 pubKey string
213 keepRunning bool
214 }{
215 {op: opA, pubKey: pubKeyA, keepRunning: true },
216 {op: opB, pubKey: pubKeyB, keepRunning: true },
217 })
218
219 changes := []ValoperChange{{OperatorAddress: opA, Power: 0 }}
220 // Build the executor directly (private function, same package).
221 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
222
223 urequire.NoError(t, newValoperChangeExecutor(cur, changes).Execute(cross(cur)))
224
225 // Effective set: only opB / pubKeyB remains.
226 effective := sysparams.GetValsetEffective()
227 urequire.Equal(t, 1 , len(effective))
228 uassert.Equal(t, pubKeyB, effective[ 0 ].PubKey)
229 }
230
231 func TestNewValidatorProposalRequest_AllowsFullValsetReplacement(cur realm, t *testing.T) {
232 resetValset(t)
233 resetCache()
234
235 testing.SetSysParamStrings(module, submodule, currKey, [] string {pubKeyA + ":10" })
236
237 opA := testutils.TestAddress( "op-A" )
238 opB := testutils.TestAddress( "op-B" )
239 seedCache(t, [] struct {
240 op address
241 pubKey string
242 keepRunning bool
243 }{
244 {op: opA, pubKey: pubKeyA, keepRunning: false },
245 {op: opB, pubKey: pubKeyB, keepRunning: true },
246 })
247
248 changes := []ValoperChange{
249 {OperatorAddress: opA, Power: 0 },
250 {OperatorAddress: opB, Power: 10 },
251 }
252 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
253
254 urequire.NoError(t, newValoperChangeExecutor(cur, changes).Execute(cross(cur)))
255
256 effective := sysparams.GetValsetEffective()
257 urequire.Equal(t, 1 , len(effective))
258 uassert.Equal(t, pubKeyB, effective[ 0 ].PubKey)
259 uassert.Equal(t, uint64 ( 10 ), effective[ 0 ].VotingPower)
260 }
261
262 func TestNewValidatorProposalRequest_RejectsKeepRunningFalseAtCreation(cur realm, t *testing.T) {
263 resetCache()
264 op := testutils.TestAddress( "op-A" )
265 seedCache(t, [] struct {
266 op address
267 pubKey string
268 keepRunning bool
269 }{{op: op, pubKey: pubKeyA, keepRunning: false }})
270
271 uassert.PanicsContains(t, cur, "KeepRunning=false" , func () {
272 _ = NewValidatorProposalRequest(cur,
273 []ValoperChange{{OperatorAddress: op, Power: 1 }},
274 "add opted-out" , "" ,
275 )
276 })
277 }
278
279 func TestNewValidatorProposalRequest_AllowsRemoveOfKeepRunningFalse(cur realm, t *testing.T) {
280 // KeepRunning=false is the operator's opt-out signal; removing
281 // such an operator must still be allowed (it's the standard
282 // exit path). Only adds are gated.
283 resetValset(t)
284 resetCache()
285
286 // Seed the valset with two operators so removing one doesn't
287 // trip the empty-valset liveness floor.
288 testing.SetSysParamStrings(module, submodule, currKey, [] string {
289 pubKeyA + ":10" ,
290 pubKeyB + ":5" ,
291 })
292
293 opA := testutils.TestAddress( "op-A" )
294 opB := testutils.TestAddress( "op-B" )
295 seedCache(t, [] struct {
296 op address
297 pubKey string
298 keepRunning bool
299 }{
300 {op: opA, pubKey: pubKeyA, keepRunning: false }, // opted out
301 {op: opB, pubKey: pubKeyB, keepRunning: true },
302 })
303
304 // Build proposal succeeds (remove path: Power=0 ignores KeepRunning).
305 pr := NewValidatorProposalRequest(cur,
306 []ValoperChange{{OperatorAddress: opA, Power: 0 }},
307 "remove opted-out opA" , "" ,
308 )
309 _ = pr
310
311 // Executor also succeeds.
312 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
313
314 urequire.NoError(t, newValoperChangeExecutor(cur, []ValoperChange{{OperatorAddress: opA, Power: 0 }}).Execute(cross(cur)))
315 }
316
317 func TestNewValidatorProposalRequest_RejectsDuplicateOp(cur realm, t *testing.T) {
318 // Each operator may appear at most once per proposal; any shape
319 // that mentions the same op twice must panic at create-time.
320 resetCache()
321 op := testutils.TestAddress( "op-A" )
322 seedCache(t, [] struct {
323 op address
324 pubKey string
325 keepRunning bool
326 }{{op: op, pubKey: pubKeyA, keepRunning: true }})
327
328 cases := [][]ValoperChange{
329 {{OperatorAddress: op, Power: 0 }, {OperatorAddress: op, Power: 7 }}, // remove + re-add
330 {{OperatorAddress: op, Power: 7 }, {OperatorAddress: op, Power: 8 }}, // double add
331 {{OperatorAddress: op, Power: 0 }, {OperatorAddress: op, Power: 0 }}, // double remove
332 }
333 for _, changes := range cases {
334 uassert.PanicsContains(t, cur, "duplicate operator in proposal" , func () {
335 _ = NewValidatorProposalRequest(cur, changes, "dup" , "" )
336 })
337 }
338 }
339
340 func TestNewValidatorProposalRequest_RejectsPowerUpdatePairForOptedOutOp(cur realm, t *testing.T) {
341 // KeepRunning=false is binding: no proposal shape can keep an
342 // opted-out operator in the active set. The dedupe rejection
343 // fires before the KR check is even reached.
344 resetCache()
345 op := testutils.TestAddress( "op-A" )
346 seedCache(t, [] struct {
347 op address
348 pubKey string
349 keepRunning bool
350 }{{op: op, pubKey: pubKeyA, keepRunning: false }})
351
352 uassert.PanicsContains(t, cur, "duplicate operator in proposal" , func () {
353 _ = NewValidatorProposalRequest(cur,
354 []ValoperChange{
355 {OperatorAddress: op, Power: 0 },
356 {OperatorAddress: op, Power: 7 },
357 },
358 "bypass attempt" , "" ,
359 )
360 })
361 }
362
363 func TestNewValidatorProposalRequest_UpsertExistingValidator(cur realm, t *testing.T) {
364 // Single-entry {op, newPower} against an op already in the
365 // effective valset must upsert: the existing entry's power is
366 // overwritten, no remove/re-add ceremony required.
367 resetValset(t)
368 resetCache()
369
370 // Seed valset with two validators; we upsert opA's power.
371 testing.SetSysParamStrings(module, submodule, currKey, [] string {
372 pubKeyA + ":1" ,
373 pubKeyB + ":1" ,
374 })
375
376 opA := testutils.TestAddress( "op-A" )
377 opB := testutils.TestAddress( "op-B" )
378 seedCache(t, [] struct {
379 op address
380 pubKey string
381 keepRunning bool
382 }{
383 {op: opA, pubKey: pubKeyA, keepRunning: true },
384 {op: opB, pubKey: pubKeyB, keepRunning: true },
385 })
386
387 changes := []ValoperChange{{OperatorAddress: opA, Power: 9 }}
388 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
389
390 urequire.NoError(t, newValoperChangeExecutor(cur, changes).Execute(cross(cur)))
391
392 effective := sysparams.GetValsetEffective()
393 powerOf := map [ string ] uint64 {}
394 for _, v := range effective {
395 powerOf[v.PubKey] = v.VotingPower
396 }
397 uassert.Equal(t, uint64 ( 9 ), powerOf[pubKeyA], "opA power upserted from 1 to 9" )
398 uassert.Equal(t, uint64 ( 1 ), powerOf[pubKeyB], "opB unchanged" )
399 }
400
401 func TestNewValidatorProposalRequest_ExecutorRejectsRaceFlippedKeepRunning(cur realm, t *testing.T) {
402 // KeepRunning=true at proposal-create time; operator flips to
403 // false BEFORE the executor runs. Race-safety check rejects.
404 resetValset(t)
405 resetCache()
406
407 op := testutils.TestAddress( "op-A" )
408 seedCache(t, [] struct {
409 op address
410 pubKey string
411 keepRunning bool
412 }{{op: op, pubKey: pubKeyA, keepRunning: true }})
413
414 changes := []ValoperChange{{OperatorAddress: op, Power: 5 }}
415 exec := newValoperChangeExecutor(cur, changes)
416
417 // Operator flips KeepRunning=false BEFORE the executor runs.
418 valoperCache.Set(op.String(), cacheEntry{
419 SigningPubKey: pubKeyA,
420 SigningAddress: mustAddr(t, pubKeyA),
421 KeepRunning: false ,
422 })
423
424 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
425
426 uassert.AbortsContains(t, cur, "KeepRunning=false at execution" , func () {
427 _ = exec.Execute(cross(cur))
428 })
429 }
430
431 func TestNewValidatorProposalRequest_NaturalRotationFlow_NoGhost(cur realm, t *testing.T) {
432 // RotateValoperSigningKey publishes valset:proposed before any
433 // subsequent executor reads, so baseline always reflects the
434 // post-rotation state. A later power-update upserts at NEW only;
435 // no OLD ghost.
436 resetValset(t)
437 resetCache()
438
439 testing.SetSysParamStrings(module, submodule, currKey, [] string {
440 pubKeyA + ":1" ,
441 pubKeyB + ":5" ,
442 })
443 opA := testutils.TestAddress( "op-A" )
444 opB := testutils.TestAddress( "op-B" )
445 seedCache(t, [] struct {
446 op address
447 pubKey string
448 keepRunning bool
449 }{
450 {op: opA, pubKey: pubKeyA, keepRunning: true },
451 {op: opB, pubKey: pubKeyB, keepRunning: true },
452 })
453
454 testing.SetRealm(testing.NewCodeRealm(valopersRealmPath))
455 RotateValoperSigningKey(cross(cur), opA, pubKeyA, pubKeyC)
456 NotifyValoperChanged(cross(cur), opA, pubKeyC, mustAddr(t, pubKeyC), true )
457
458 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/gov/dao/impl/v0" ))
459 urequire.NoError(t, newValoperChangeExecutor(cur,
460 []ValoperChange{{OperatorAddress: opA, Power: 2 }},
461 ).Execute(cross(cur)))
462
463 effective := sysparams.GetValsetEffective()
464 powerOf := map [ string ] uint64 {}
465 for _, v := range effective {
466 powerOf[v.PubKey] = v.VotingPower
467 }
468 uassert.Equal(t, uint64 ( 2 ), powerOf[pubKeyC], "opA published at NEW power=2" )
469 uassert.Equal(t, uint64 ( 5 ), powerOf[pubKeyB], "opB unchanged" )
470 _, ghost := powerOf[pubKeyA]
471 uassert.False(t, ghost, "OLD signing key (pubKeyA) must not linger in valset" )
472 urequire.Equal(t, 2 , len(effective), "exactly two entries — opA(NEW), opB" )
473 }
474
475 func TestNewValidatorProposalRequest_PhantomBaselineDocumentsUnreachableState(cur realm, t *testing.T) {
476 // Pin executor behavior on a phantom state (cache=NEW,
477 // valset:current=OLD, dirty=false) — unreachable via natural
478 // flow because RotateValoperSigningKey publishes proposed
479 // before NotifyValoperChanged updates the cache. If a future
480 // code path ever updates the cache without going through
481 // Rotate, the asymmetry would be a real bug; this test pins
482 // the current behavior so the divergence surfaces.
483 resetValset(t)
484 resetCache()
485
486 testing.SetSysParamStrings(module, submodule, currKey, [] string {
487 pubKeyA + ":1" ,
488 pubKeyB + ":5" ,
489 })
490
491 opA := testutils.TestAddress( "op-A" )
492 opB := testutils.TestAddress( "op-B" )
493 seedCache(t, [] struct {
494 op address
495 pubKey string
496 keepRunning bool
497 }{
498 {op: opA, pubKey: pubKeyC, keepRunning: true },
499 {op: opB, pubKey: pubKeyB, keepRunning: true },
500 })
501
502 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
503
504 urequire.NoError(t, newValoperChangeExecutor(cur,
505 []ValoperChange{{OperatorAddress: opA, Power: 2 }},
506 ).Execute(cross(cur)))
507
508 effective := sysparams.GetValsetEffective()
509 powerOf := map [ string ] uint64 {}
510 for _, v := range effective {
511 powerOf[v.PubKey] = v.VotingPower
512 }
513 uassert.Equal(t, uint64 ( 1 ), powerOf[pubKeyA], "phantom OLD lingers from baseline" )
514 uassert.Equal(t, uint64 ( 2 ), powerOf[pubKeyC], "executor upserts at NEW" )
515 uassert.Equal(t, uint64 ( 5 ), powerOf[pubKeyB], "opB unchanged" )
516 urequire.Equal(t, 3 , len(effective),
517 "three entries — phantom-state ghost; this state is unreachable via natural flow" )
518 }
519
520 func TestNewValidatorProposalRequest_SameBlockExecuteThenRotate(cur realm, t *testing.T) {
521 // Same-block ordering: proposal-execute writes proposed
522 // (dirty=true); a subsequent rotation reads proposed-when-dirty
523 // and accumulates the prior power change rather than clobbering
524 // back to current.
525 resetValset(t)
526 resetCache()
527
528 testing.SetSysParamStrings(module, submodule, currKey, [] string {
529 pubKeyA + ":1" ,
530 pubKeyB + ":5" ,
531 })
532
533 opA := testutils.TestAddress( "op-A" )
534 opB := testutils.TestAddress( "op-B" )
535 seedCache(t, [] struct {
536 op address
537 pubKey string
538 keepRunning bool
539 }{
540 {op: opA, pubKey: pubKeyA, keepRunning: true },
541 {op: opB, pubKey: pubKeyB, keepRunning: true },
542 })
543
544 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
545
546 urequire.NoError(t, newValoperChangeExecutor(cur,
547 []ValoperChange{{OperatorAddress: opA, Power: 3 }},
548 ).Execute(cross(cur)))
549
550 testing.SetRealm(testing.NewCodeRealm(valopersRealmPath))
551 RotateValoperSigningKey(cross(cur), opA, pubKeyA, pubKeyC)
552 NotifyValoperChanged(cross(cur), opA, pubKeyC, mustAddr(t, pubKeyC), true )
553
554 effective := sysparams.GetValsetEffective()
555 powerOf := map [ string ] uint64 {}
556 for _, v := range effective {
557 powerOf[v.PubKey] = v.VotingPower
558 }
559 uassert.Equal(t, uint64 ( 3 ), powerOf[pubKeyC], "rotation accumulates with prior upsert; final power=3" )
560 uassert.Equal(t, uint64 ( 5 ), powerOf[pubKeyB], "opB unchanged" )
561 _, gotOld := powerOf[pubKeyA]
562 uassert.False(t, gotOld, "OLD signing key (pubKeyA) must not appear after rotation" )
563 urequire.Equal(t, 2 , len(effective), "exactly two entries — opA(NEW) and opB" )
564 }
565
566 // TestNewValidatorProposalRequest_ExecutorVanishedCacheEntryPanics pins
567 // the "operator vanished from valoperCache between propose and execute"
568 // branch. No production path deletes from valoperCache today (only Set
569 // is called by NotifyValoperChanged), but bptree.BPTree exposes Remove
570 // so the underlying data structure does support deletion. This test
571 // simulates that hypothetical state directly via the package-private
572 // cache var to confirm the executor panics with the documented message
573 // rather than silently mis-publishing an empty/wrong valset.
574 //
575 // If a future commit ever introduces a public cache-delete path, this
576 // test still passes — it's a contract-pinning test for the panic itself.
577 // If the team decides the branch is unreachable enough to drop, this
578 // test is the first thing to break.
579 func TestNewValidatorProposalRequest_ExecutorVanishedCacheEntryPanics(cur realm, t *testing.T) {
580 resetValset(t)
581 resetCache()
582
583 op := testutils.TestAddress( "op-A" )
584 seedCache(t, [] struct {
585 op address
586 pubKey string
587 keepRunning bool
588 }{{op: op, pubKey: pubKeyA, keepRunning: true }})
589
590 exec := newValoperChangeExecutor(cur,
591 []ValoperChange{{OperatorAddress: op, Power: 5 }},
592 )
593
594 // Simulate the unreachable-today state: cache entry deleted between
595 // proposal-create and proposal-execute. Direct package-private mutation
596 // (NOT a public API) — production code has no path here today.
597 _, removed := valoperCache.Remove(op.String())
598 urequire.True(t, removed, "fixture: cache entry must have been present before Remove" )
599
600 testing.SetRealm(testing.NewCodeRealm(asGovDAOProxy))
601
602 uassert.AbortsContains(t, cur, "operator vanished from valoperCache between propose and execute" , func () {
603 _ = exec.Execute(cross(cur))
604 })
605 }
606
607 // contains is a tiny strings.Contains shim so tests don't import a
608 // new package.
609 func contains(s, substr string ) bool {
610 for i := 0 ; i+len(substr) <= len(s); i++ {
611 if s[i:i+len(substr)] == substr {
612 return true
613 }
614 }
615 return false
616 }
617