Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}