rotate_test.gno
7.52 Kb · 194 lines
1package valopers
2
3import (
4 "chain"
5 "testing"
6
7 "gno.land/p/nt/testutils/v0"
8 "gno.land/p/nt/uassert/v0"
9 "gno.land/p/nt/urequire/v0"
10)
11
12const (
13 // Second valid pubkey used as the rotation target. Distinct from
14 // validValidatorInfo's pubkey so the signingRegistry uniqueness
15 // check is exercised.
16 rotateTargetPubKey = "gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq3ds6sdvc0shfkq02h6xx5g0jp04aadexfnpsmgjxu72xz9y30aqfrlpny"
17
18 // Test-local mirror of the rotation_period_blocks sys-param's
19 // default. resetState() seeds the same value into sysparams.
20 testRotationPeriodBlocks = int64(600)
21)
22
23// registerForRotation does the boilerplate setup that every rotation
24// test starts from: clear state, register the valoper as info.Address,
25// and return the info struct. rlm is threaded into Register via
26// cross(rlm) — it's a non-first parameter so the helper stays a
27// regular (non-crossing) function.
28func registerForRotation(t *testing.T, rlm realm) struct {
29 Moniker string
30 Description string
31 ServerType string
32 Address address
33 PubKey string
34} {
35 t.Helper()
36 resetState()
37 info := validValidatorInfo(t)
38 testing.SetRealm(testing.NewUserRealm(info.Address))
39 testing.SetOriginSend(chain.Coins{minFee})
40 Register(cross(rlm), info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)
41 return info
42}
43
44func TestUpdateSigningKey_HappyPath(cur realm, t *testing.T) {
45 info := registerForRotation(t, cur)
46
47 // Advance past the throttle window. SkipHeights doesn't preserve
48 // the realm context across the boundary, so re-set it before the
49 // next cross-call.
50 testing.SkipHeights(testRotationPeriodBlocks + 1)
51 testing.SetRealm(testing.NewUserRealm(info.Address))
52
53 uassert.NotAborts(t, cur, func() {
54 UpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)
55 })
56
57 v := GetByAddr(info.Address)
58 uassert.Equal(t, rotateTargetPubKey, v.SigningPubKey)
59
60 newSigningAddr, err := chain.PubKeyAddress(rotateTargetPubKey)
61 urequire.NoError(t, err)
62 uassert.Equal(t, newSigningAddr, v.SigningAddress)
63
64 // New entry active in registry.
65 rawNew, ok := signingRegistry.Get(newSigningAddr.String())
66 urequire.True(t, ok, "new signing addr in registry")
67 newEntry := rawNew.(regEntry)
68 uassert.Equal(t, info.Address, newEntry.OperatorAddress)
69 uassert.Equal(t, int64(0), newEntry.RetiredAtHeight)
70
71 // Old entry retired (still present, RetiredAtHeight > 0).
72 oldSigningAddr, err := chain.PubKeyAddress(info.PubKey)
73 urequire.NoError(t, err)
74 rawOld, ok := signingRegistry.Get(oldSigningAddr.String())
75 urequire.True(t, ok, "old signing addr retained as retired")
76 oldEntry := rawOld.(regEntry)
77 uassert.Equal(t, info.Address, oldEntry.OperatorAddress)
78 uassert.True(t, oldEntry.RetiredAtHeight > 0, "old entry must be retired (RetiredAtHeight > 0)")
79}
80
81func TestUpdateSigningKey_ThrottleRejection(cur realm, t *testing.T) {
82 info := registerForRotation(t, cur)
83 testing.SetRealm(testing.NewUserRealm(info.Address))
84
85 // Try to rotate immediately, before throttle elapses.
86 uassert.AbortsWithMessage(t, cur, ErrRotationThrottled.Error(), func() {
87 UpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)
88 })
89
90 // Advance just shy of the threshold; still rejected.
91 testing.SkipHeights(testRotationPeriodBlocks - 1)
92 testing.SetRealm(testing.NewUserRealm(info.Address))
93 uassert.AbortsWithMessage(t, cur, ErrRotationThrottled.Error(), func() {
94 UpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)
95 })
96
97 // One more block — now exactly at the threshold; allowed.
98 testing.SkipHeights(1)
99 testing.SetRealm(testing.NewUserRealm(info.Address))
100 uassert.NotAborts(t, cur, func() {
101 UpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)
102 })
103}
104
105func TestUpdateSigningKey_RejectsNonAuthListCaller(cur realm, t *testing.T) {
106 info := registerForRotation(t, cur)
107 testing.SkipHeights(testRotationPeriodBlocks + 1)
108
109 // Switch to a caller not on the auth list.
110 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("attacker")))
111
112 // Pin the exact authorizable error so this catches an auth
113 // regression rather than any abort. Empty-substring match (the
114 // previous form) accepted any panic, including unrelated ones.
115 uassert.AbortsContains(t, cur, "not in authorized list", func() {
116 UpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)
117 })
118}
119
120func TestUpdateSigningKey_RejectsReuseOfActiveKey(cur realm, t *testing.T) {
121 info := registerForRotation(t, cur)
122 testing.SkipHeights(testRotationPeriodBlocks + 1)
123 testing.SetRealm(testing.NewUserRealm(info.Address))
124
125 // Try to "rotate" to the same key — already in registry.
126 uassert.AbortsWithMessage(t, cur, ErrSigningKeyTaken.Error(), func() {
127 UpdateSigningKey(cross(cur), info.Address, info.PubKey)
128 })
129}
130
131func TestUpdateSigningKey_RejectsRotationOntoActiveValidator(cur realm, t *testing.T) {
132 // Front-running guard: a rotation whose derived signing address is
133 // already an active validator must be rejected. Mirrors the same
134 // guard in Register (valopers.gno: ErrFrontrunValidator). Without
135 // this, any valoper could rotate onto an unregistered (e.g.
136 // genesis-seeded) validator slot — v3's executor would overwrite
137 // the active validator's entry in the effective valset with this
138 // operator's claim, then a subsequent govDAO remove-op proposal
139 // would delete it. signingRegistry uniqueness alone doesn't catch
140 // this case: a genesis-seeded validator never went through
141 // Register or UpdateSigningKey, so its signing address is absent
142 // from signingRegistry.
143 info := registerForRotation(t, cur)
144
145 // Seed v3's valset:current with the address that rotateTargetPubKey
146 // derives to. ValsetEffective is read from valset:current when the
147 // dirty flag is unset, which is the default.
148 testing.SetSysParamStrings("node", "valset", "current",
149 []string{rotateTargetPubKey + ":1"})
150
151 testing.SkipHeights(testRotationPeriodBlocks + 1)
152 testing.SetRealm(testing.NewUserRealm(info.Address))
153
154 uassert.AbortsWithMessage(t, cur, ErrFrontrunValidator.Error(), func() {
155 UpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)
156 })
157
158 // Cleanup: clear the seeded valset to avoid leaking into sibling
159 // subtests if the runner doesn't reset package state between them.
160 testing.SetSysParamStrings("node", "valset", "current", []string{})
161}
162
163func TestUpdateSigningKey_RejectsReuseOfRetiredKey(cur realm, t *testing.T) {
164 info := registerForRotation(t, cur)
165
166 // First rotation: info.PubKey -> rotateTargetPubKey. info.PubKey
167 // becomes retired.
168 testing.SkipHeights(testRotationPeriodBlocks + 1)
169 testing.SetRealm(testing.NewUserRealm(info.Address))
170 UpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)
171
172 // Second rotation back to info.PubKey must fail — it's retired
173 // but signingRegistry retains it forever.
174 testing.SkipHeights(testRotationPeriodBlocks + 1)
175 testing.SetRealm(testing.NewUserRealm(info.Address))
176 uassert.AbortsWithMessage(t, cur, ErrSigningKeyTaken.Error(), func() {
177 UpdateSigningKey(cross(cur), info.Address, info.PubKey)
178 })
179}
180
181func TestUpdateSigningKey_LastRotationHeightAdvances(cur realm, t *testing.T) {
182 info := registerForRotation(t, cur)
183 testing.SkipHeights(testRotationPeriodBlocks + 1)
184 testing.SetRealm(testing.NewUserRealm(info.Address))
185 UpdateSigningKey(cross(cur), info.Address, rotateTargetPubKey)
186
187 v := GetByAddr(info.Address)
188 uassert.True(t, v.LastRotationHeight > 0, "LastRotationHeight set on rotation")
189
190 // Immediately after, throttle rejects again.
191 uassert.AbortsWithMessage(t, cur, ErrRotationThrottled.Error(), func() {
192 UpdateSigningKey(cross(cur), info.Address, "gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqddddqg2glc8x4fl7vxjlnr7p5a3czm5kcdp4239sg6yqdc4rc2r5cjrffs")
193 })
194}