cache_test.gno
8.12 Kb · 252 lines
1package validators
2
3import (
4 "testing"
5
6 "gno.land/p/nt/bptree/v0"
7 "gno.land/p/nt/testutils/v0"
8 "gno.land/p/nt/uassert/v0"
9 "gno.land/p/nt/urequire/v0"
10 sysparams "gno.land/r/sys/params"
11)
12
13// cur is a zero-value realm used as a placeholder when forwarding to
14// uassert/urequire dispatch helpers that gained an `rlm realm` param.
15// These tests pass `func()` callbacks (no crossing inside the callback),
16// so rlm is ignored — a nil realm here is safe.
17var cur realm
18
19// resetCache clears valoperCache so subtests don't leak state.
20func resetCache() {
21 valoperCache = bptree.NewBPTree32()
22}
23
24func TestNotifyValoperChanged_HappyPath(cur realm, t *testing.T) {
25 resetCache()
26
27 op := testutils.TestAddress("op-A")
28 signingAddr := mustAddr(t, pubKeyA)
29
30 // Caller realm = valopers; auth check passes.
31 testing.SetRealm(testing.NewCodeRealm(valopersRealmPath))
32
33 NotifyValoperChanged(cross(cur), op, pubKeyA, signingAddr, true)
34
35 rawEntry, ok := valoperCache.Get(op.String())
36 urequire.True(t, ok, "cache entry must exist")
37 entry := rawEntry.(cacheEntry)
38 uassert.Equal(t, pubKeyA, entry.SigningPubKey)
39 uassert.Equal(t, signingAddr, entry.SigningAddress)
40 uassert.Equal(t, true, entry.KeepRunning)
41
42 // Subsequent call updates the same slot.
43 signingAddrB := mustAddr(t, pubKeyB)
44 NotifyValoperChanged(cross(cur), op, pubKeyB, signingAddrB, false)
45
46 rawEntry, ok = valoperCache.Get(op.String())
47 urequire.True(t, ok)
48 entry = rawEntry.(cacheEntry)
49 uassert.Equal(t, pubKeyB, entry.SigningPubKey)
50 uassert.Equal(t, signingAddrB, entry.SigningAddress)
51 uassert.Equal(t, false, entry.KeepRunning)
52}
53
54func TestNotifyValoperChanged_RejectsNonValopersCaller(cur realm, t *testing.T) {
55 resetCache()
56
57 op := testutils.TestAddress("op-A")
58 signingAddr := mustAddr(t, pubKeyA)
59
60 // Caller realm = some other realm; auth check rejects.
61 testing.SetRealm(testing.NewCodeRealm("gno.land/r/attacker"))
62
63 uassert.AbortsContains(t, cur, "caller realm must be "+valopersRealmPath, func() {
64 NotifyValoperChanged(cross(cur), op, pubKeyA, signingAddr, true)
65 })
66
67 // User MsgCall — UserRealm has empty pkgpath; rejected the same way.
68 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("user")))
69
70 uassert.AbortsContains(t, cur, "caller realm must be "+valopersRealmPath, func() {
71 NotifyValoperChanged(cross(cur), op, pubKeyA, signingAddr, true)
72 })
73}
74
75func TestRotateValoperSigningKey_AppliesRotation(cur realm, t *testing.T) {
76 resetValset(t)
77 resetCache()
78
79 // Seed the valset with op-A signing under pubKeyA at power=10.
80 testing.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + ":10"})
81
82 op := testutils.TestAddress("op-A")
83 testing.SetRealm(testing.NewCodeRealm(valopersRealmPath))
84
85 RotateValoperSigningKey(cross(cur), op, pubKeyA, pubKeyB)
86
87 // After rotation the effective set (proposed-when-dirty) should
88 // contain pubKeyB at the same power; pubKeyA should be gone.
89 uassert.True(t, sysparams.ValsetDirty(), "dirty bit set after RotateValoperSigningKey")
90
91 effective := sysparams.GetValsetEffective()
92 urequire.Equal(t, 1, len(effective))
93 uassert.Equal(t, pubKeyB, effective[0].PubKey)
94 uassert.Equal(t, uint64(10), effective[0].VotingPower)
95}
96
97func TestRotateValoperSigningKey_OperatorNotInValset_NoOp(cur realm, t *testing.T) {
98 resetValset(t)
99 resetCache()
100
101 // Empty valset; the operator is not currently signing.
102 op := testutils.TestAddress("op-A")
103 testing.SetRealm(testing.NewCodeRealm(valopersRealmPath))
104
105 // Should not panic; no sysparams write expected (dirty stays false).
106 uassert.NotAborts(t, cur, func() {
107 RotateValoperSigningKey(cross(cur), op, pubKeyA, pubKeyB)
108 })
109
110 uassert.False(t, sysparams.ValsetDirty(), "dirty bit must remain false on no-op rotation")
111 uassert.Equal(t, 0, len(sysparams.GetValsetEffective()))
112}
113
114func TestRotateValoperSigningKey_RejectsNonValopersCaller(cur realm, t *testing.T) {
115 resetValset(t)
116 resetCache()
117 testing.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + ":10"})
118
119 op := testutils.TestAddress("op-A")
120
121 // Foreign realm — rejected.
122 testing.SetRealm(testing.NewCodeRealm("gno.land/r/attacker"))
123 uassert.AbortsContains(t, cur, "caller realm must be "+valopersRealmPath, func() {
124 RotateValoperSigningKey(cross(cur), op, pubKeyA, pubKeyB)
125 })
126
127 // User MsgCall — UserRealm pkgpath is "" — rejected.
128 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("user")))
129 uassert.AbortsContains(t, cur, "caller realm must be "+valopersRealmPath, func() {
130 RotateValoperSigningKey(cross(cur), op, pubKeyA, pubKeyB)
131 })
132
133 // Sanity: the valset wasn't mutated by the rejected calls (dirty
134 // bit stays false; effective set still equals the seeded current).
135 uassert.False(t, sysparams.ValsetDirty())
136 effective := sysparams.GetValsetEffective()
137 urequire.Equal(t, 1, len(effective))
138 uassert.Equal(t, pubKeyA, effective[0].PubKey)
139}
140
141func TestAssertGenesisValopersConsistent_HappyPath(cur realm, t *testing.T) {
142 resetValset(t)
143 resetCache()
144 testing.SetHeight(0) // assertion is genesis-mode only
145
146 // Seed valset:current with two genesis validators.
147 testing.SetSysParamStrings(module, submodule, currKey, []string{
148 pubKeyA + ":10",
149 pubKeyB + ":5",
150 })
151
152 // Seed valoperCache with profiles whose SigningAddress matches.
153 opA := testutils.TestAddress("op-A")
154 opB := testutils.TestAddress("op-B")
155 seedCache(t, []struct {
156 op address
157 pubKey string
158 keepRunning bool
159 }{
160 {op: opA, pubKey: pubKeyA, keepRunning: true},
161 {op: opB, pubKey: pubKeyB, keepRunning: true},
162 })
163
164 uassert.NotAborts(t, cur, func() {
165 AssertGenesisValopersConsistent(cross(cur))
166 })
167}
168
169func TestAssertGenesisValopersConsistent_PanicsOnMissingProfile(cur realm, t *testing.T) {
170 resetValset(t)
171 resetCache()
172 testing.SetHeight(0)
173
174 // Two validators in valset:current but only one has a profile.
175 testing.SetSysParamStrings(module, submodule, currKey, []string{
176 pubKeyA + ":10",
177 pubKeyB + ":5",
178 })
179
180 opA := testutils.TestAddress("op-A")
181 seedCache(t, []struct {
182 op address
183 pubKey string
184 keepRunning bool
185 }{
186 {op: opA, pubKey: pubKeyA, keepRunning: true},
187 // op for pubKeyB intentionally missing.
188 })
189
190 uassert.AbortsContains(t, cur, "no corresponding valoper profile", func() {
191 AssertGenesisValopersConsistent(cross(cur))
192 })
193}
194
195func TestAssertGenesisValopersConsistent_EmptyValsetTrivial(cur realm, t *testing.T) {
196 resetValset(t)
197 resetCache()
198 testing.SetHeight(0)
199
200 // Empty valset → assertion trivially holds (no entries to check).
201 uassert.NotAborts(t, cur, func() {
202 AssertGenesisValopersConsistent(cross(cur))
203 })
204}
205
206func TestAssertGenesisValopersConsistent_RejectsPostGenesis(cur realm, t *testing.T) {
207 resetValset(t)
208 resetCache()
209 // Default test height is non-zero (123); the assertion must
210 // refuse to run outside genesis-mode replay.
211 uassert.AbortsContains(t, cur, "only callable during genesis-mode replay", func() {
212 AssertGenesisValopersConsistent(cross(cur))
213 })
214}
215
216func TestRotateValoperSigningKey_AccumulatesAcrossSameBlock(cur realm, t *testing.T) {
217 resetValset(t)
218 resetCache()
219
220 // Seed valset with two operators signing under pubKeyA and pubKeyC.
221 testing.SetSysParamStrings(module, submodule, currKey, []string{
222 pubKeyA + ":10",
223 pubKeyC + ":5",
224 })
225
226 opA := testutils.TestAddress("op-A")
227 opC := testutils.TestAddress("op-C")
228 testing.SetRealm(testing.NewCodeRealm(valopersRealmPath))
229
230 // First rotation: A → B. Read-modify-write of GetValsetEffective.
231 RotateValoperSigningKey(cross(cur), opA, pubKeyA, pubKeyB)
232
233 // Second rotation in the same block: C → A. Should not clobber
234 // the first rotation; should read proposed-when-dirty as baseline.
235 RotateValoperSigningKey(cross(cur), opC, pubKeyC, pubKeyA)
236
237 // Final effective set: {pubKeyB:10, pubKeyA:5}. Order may differ
238 // because GetValsetEffective parses sysparams' sorted-string slot
239 // and returns []Validator preserving that order.
240 effective := sysparams.GetValsetEffective()
241 urequire.Equal(t, 2, len(effective))
242
243 // Build a power-by-pubkey map and assert.
244 powerOf := map[string]uint64{}
245 for _, v := range effective {
246 powerOf[v.PubKey] = v.VotingPower
247 }
248 uassert.Equal(t, uint64(10), powerOf[pubKeyB])
249 uassert.Equal(t, uint64(5), powerOf[pubKeyA])
250 _, ok := powerOf[pubKeyC]
251 uassert.False(t, ok, "pubKeyC should be replaced by pubKeyA at the rotated slot")
252}