limits_test.gno
16.94 Kb · 531 lines
1package validators
2
3import (
4 "testing"
5 "time"
6
7 "gno.land/p/nt/testutils/v0"
8 "gno.land/p/nt/uassert/v0"
9 "gno.land/p/nt/urequire/v0"
10)
11
12// resetLimits restores the cooldown + trust-level state to its package
13// default. Required because gno's test runner does not isolate
14// package-level vars across top-level tests — once any test runs a
15// successful proposal executor, lastValsetUpdate is set to time.Now()
16// and subsequent tests would trip the cooldown check unless reset.
17//
18// Pair with resetCache() / resetValset(t) at the top of any test that
19// drives a valset proposal through to execution.
20func resetLimits() {
21 lastValsetUpdate = time.Time{}
22 valsetUpdateCooldown = 24 * time.Hour
23 trustLevelRatio = trustRatio{numerator: 1, denominator: 3}
24}
25
26func TestNewValidatorProposalRequest_RejectsWithinCooldown(t *testing.T) {
27 // Creation-time guard: a proposal cannot even enter GovDAO if the
28 // previous valset update was within the cooldown window.
29 resetValset(t)
30 resetCache()
31 resetLimits()
32 lastValsetUpdate = time.Now()
33
34 op := testutils.TestAddress("op-A")
35 seedCache(t, []struct {
36 op address
37 pubKey string
38 keepRunning bool
39 }{{op: op, pubKey: pubKeyA, keepRunning: true}})
40
41 uassert.PanicsContains(t, errValsetUpdateCooldown, func() {
42 _ = NewValidatorProposalRequest(
43 []ValoperChange{{OperatorAddress: op, Power: 1}},
44 "add during cooldown", "",
45 )
46 })
47}
48
49func TestNewValidatorProposalRequest_ExecutorRejectsWithinCooldown(t *testing.T) {
50 // Execution-time guard (the binding source of truth): a proposal
51 // that was created when cooldown was clear can still be rejected
52 // at execute-time if another proposal slipped in between.
53 resetValset(t)
54 resetCache()
55 resetLimits()
56
57 testing.SetSysParamStrings(module, submodule, currKey, []string{pubKeyA + ":1"})
58 opA := testutils.TestAddress("op-A")
59 opB := testutils.TestAddress("op-B")
60 seedCache(t, []struct {
61 op address
62 pubKey string
63 keepRunning bool
64 }{
65 {op: opA, pubKey: pubKeyA, keepRunning: true},
66 {op: opB, pubKey: pubKeyB, keepRunning: true},
67 })
68
69 exec := newValoperChangeExecutor([]ValoperChange{{OperatorAddress: opB, Power: 1}}, trustLevelRatio)
70
71 // Simulate another proposal having just landed between creation
72 // and execution of this one.
73 lastValsetUpdate = time.Now()
74
75 uassert.AbortsContains(t, errValsetUpdateCooldown, func() {
76 _ = exec.Execute(cross)
77 })
78}
79
80func TestNewValidatorProposalRequest_AllowsAfterCooldown(t *testing.T) {
81 // Happy path: cooldown elapsed → proposal applies → lastValsetUpdate
82 // is bumped forward by the executor.
83 resetValset(t)
84 resetCache()
85 resetLimits()
86
87 testing.SetSysParamStrings(module, submodule, currKey, []string{
88 pubKeyA + ":10",
89 pubKeyB + ":5",
90 })
91 opA := testutils.TestAddress("op-A")
92 opB := testutils.TestAddress("op-B")
93 seedCache(t, []struct {
94 op address
95 pubKey string
96 keepRunning bool
97 }{
98 {op: opA, pubKey: pubKeyA, keepRunning: true},
99 {op: opB, pubKey: pubKeyB, keepRunning: true},
100 })
101
102 lastValsetUpdate = time.Now().Add(-valsetUpdateCooldown - time.Minute)
103
104 exec := newValoperChangeExecutor([]ValoperChange{{OperatorAddress: opA, Power: 11}}, trustLevelRatio)
105 urequire.NoError(t, exec.Execute(cross))
106
107 // Executor should have refreshed the cooldown clock.
108 uassert.True(t, time.Since(lastValsetUpdate) < time.Minute,
109 "executor must bump lastValsetUpdate on success")
110}
111
112func TestNewValidatorProposalRequest_RejectsTrustLevelViolation(t *testing.T) {
113 // Baseline: A:10, B:1, C:1 (prevTotal=12, trust threshold = 4 with
114 // 1/3 ratio). Proposal removes A. Retained = 2 (B+C). 2 < 4 →
115 // trust-level violation. Uses only A/B/C so we don't need extra
116 // pubkeys.
117 resetValset(t)
118 resetCache()
119 resetLimits()
120
121 testing.SetSysParamStrings(module, submodule, currKey, []string{
122 pubKeyA + ":10",
123 pubKeyB + ":1",
124 pubKeyC + ":1",
125 })
126 opA := testutils.TestAddress("op-A")
127 opB := testutils.TestAddress("op-B")
128 opC := testutils.TestAddress("op-C")
129 seedCache(t, []struct {
130 op address
131 pubKey string
132 keepRunning bool
133 }{
134 {op: opA, pubKey: pubKeyA, keepRunning: true},
135 {op: opB, pubKey: pubKeyB, keepRunning: true},
136 {op: opC, pubKey: pubKeyC, keepRunning: true},
137 })
138
139 exec := newValoperChangeExecutor([]ValoperChange{{OperatorAddress: opA, Power: 0}}, trustLevelRatio)
140
141 uassert.AbortsContains(t, errTrustLevelViolated, func() {
142 _ = exec.Execute(cross)
143 })
144}
145
146func TestNewValidatorProposalRequest_RejectsAtTrustLevelBoundary(t *testing.T) {
147 // Baseline: A:4, B:3, C:2 (prevTotal=9, threshold = 9/3 = 3).
148 // Proposal removes A and C, retaining only B. retained (BASELINE
149 // VP of survivors) = 3, exactly equal to threshold. 3*3 = 9, 9*1
150 // = 9, 9 <= 9 ⇒ true ⇒ aborts.
151 //
152 // This pins the boundary as STRICT — talliedVotingPower must be
153 // strictly greater than the threshold to pass. Matches the
154 // CometBFT light-client rule (verifyCommitSingle:
155 // "if got <= needed return ErrNotEnoughVotingPowerSigned").
156 resetValset(t)
157 resetCache()
158 resetLimits()
159
160 testing.SetSysParamStrings(module, submodule, currKey, []string{
161 pubKeyA + ":4",
162 pubKeyB + ":3",
163 pubKeyC + ":2",
164 })
165 opA := testutils.TestAddress("op-A")
166 opB := testutils.TestAddress("op-B")
167 opC := testutils.TestAddress("op-C")
168 seedCache(t, []struct {
169 op address
170 pubKey string
171 keepRunning bool
172 }{
173 {op: opA, pubKey: pubKeyA, keepRunning: true},
174 {op: opB, pubKey: pubKeyB, keepRunning: true},
175 {op: opC, pubKey: pubKeyC, keepRunning: true},
176 })
177
178 exec := newValoperChangeExecutor([]ValoperChange{
179 {OperatorAddress: opA, Power: 0},
180 {OperatorAddress: opC, Power: 0},
181 }, trustLevelRatio)
182
183 uassert.AbortsContains(t, errTrustLevelViolated, func() {
184 _ = exec.Execute(cross)
185 })
186}
187
188func TestNewValidatorProposalRequest_AllowsJustAboveTrustLevel(t *testing.T) {
189 // Companion to RejectsAtTrustLevelBoundary: same baseline, but
190 // retained = threshold + 1 ⇒ must pass. Brackets the boundary on
191 // the passing side so an off-by-one in the comparison surfaces.
192 // Baseline: A:4, B:3, C:2 (prevTotal=9, threshold = 3).
193 // Proposal removes B and C, retaining only A. retained = 4.
194 // 4*3 = 12, 9*1 = 9, 12 <= 9 ⇒ false ⇒ passes.
195 resetValset(t)
196 resetCache()
197 resetLimits()
198
199 testing.SetSysParamStrings(module, submodule, currKey, []string{
200 pubKeyA + ":4",
201 pubKeyB + ":3",
202 pubKeyC + ":2",
203 })
204 opA := testutils.TestAddress("op-A")
205 opB := testutils.TestAddress("op-B")
206 opC := testutils.TestAddress("op-C")
207 seedCache(t, []struct {
208 op address
209 pubKey string
210 keepRunning bool
211 }{
212 {op: opA, pubKey: pubKeyA, keepRunning: true},
213 {op: opB, pubKey: pubKeyB, keepRunning: true},
214 {op: opC, pubKey: pubKeyC, keepRunning: true},
215 })
216
217 exec := newValoperChangeExecutor([]ValoperChange{
218 {OperatorAddress: opB, Power: 0},
219 {OperatorAddress: opC, Power: 0},
220 }, trustLevelRatio)
221
222 urequire.NoError(t, exec.Execute(cross))
223}
224
225func TestNewValidatorProposalRequest_RejectsJustBelowTrustLevel(t *testing.T) {
226 // Companion to RejectsAtTrustLevelBoundary: same baseline, but
227 // retained = threshold-1 ⇒ must fail. Brackets the boundary on
228 // the failing side so an off-by-one in the comparison surfaces.
229 // Baseline: A:4, B:3, C:2 (prevTotal=9, threshold = 3).
230 // Proposal removes A and B, retaining only C. retained = 2.
231 // 2*3 = 6, 9*1 = 9, 6 <= 9 ⇒ true ⇒ aborts.
232 resetValset(t)
233 resetCache()
234 resetLimits()
235
236 testing.SetSysParamStrings(module, submodule, currKey, []string{
237 pubKeyA + ":4",
238 pubKeyB + ":3",
239 pubKeyC + ":2",
240 })
241 opA := testutils.TestAddress("op-A")
242 opB := testutils.TestAddress("op-B")
243 opC := testutils.TestAddress("op-C")
244 seedCache(t, []struct {
245 op address
246 pubKey string
247 keepRunning bool
248 }{
249 {op: opA, pubKey: pubKeyA, keepRunning: true},
250 {op: opB, pubKey: pubKeyB, keepRunning: true},
251 {op: opC, pubKey: pubKeyC, keepRunning: true},
252 })
253
254 exec := newValoperChangeExecutor([]ValoperChange{
255 {OperatorAddress: opA, Power: 0},
256 {OperatorAddress: opB, Power: 0},
257 }, trustLevelRatio)
258
259 uassert.AbortsContains(t, errTrustLevelViolated, func() {
260 _ = exec.Execute(cross)
261 })
262}
263
264func TestNewValidatorProposalRequest_ExecutorUsesSnapshotTrustLevel(t *testing.T) {
265 // Snapshot semantics: the trust-level rule the executor enforces
266 // is the ratio that was in effect at proposal-creation time, NOT
267 // the package-level trustLevelRatio at execute-time. This closes
268 // the same-block ordering attack: a trust-level-loosening
269 // proposal executed first can't relax the rule for a valset-
270 // change proposal that was created under the stricter rule.
271 //
272 // Baseline: A:1, B:1, C:1 (prevTotal=3). Proposal removes A.
273 // retained (BASELINE VP of survivors) = 2 (B+C).
274 // under 1/3 ratio: 2*3=6, 3*1=3, 6 <= 3 ⇒ false ⇒ would pass
275 // under 2/3 ratio: 2*3=6, 3*2=6, 6 <= 6 ⇒ true ⇒ would fail
276 // We snapshot at 2/3, then drop the package ratio to 1/3, then
277 // execute — the snapshot wins.
278 resetValset(t)
279 resetCache()
280 resetLimits()
281
282 testing.SetSysParamStrings(module, submodule, currKey, []string{
283 pubKeyA + ":1",
284 pubKeyB + ":1",
285 pubKeyC + ":1",
286 })
287 opA := testutils.TestAddress("op-A")
288 opB := testutils.TestAddress("op-B")
289 opC := testutils.TestAddress("op-C")
290 seedCache(t, []struct {
291 op address
292 pubKey string
293 keepRunning bool
294 }{
295 {op: opA, pubKey: pubKeyA, keepRunning: true},
296 {op: opB, pubKey: pubKeyB, keepRunning: true},
297 {op: opC, pubKey: pubKeyC, keepRunning: true},
298 })
299
300 // Build the executor under the stricter ratio.
301 stricter := trustRatio{numerator: 2, denominator: 3}
302 exec := newValoperChangeExecutor([]ValoperChange{
303 {OperatorAddress: opA, Power: 0},
304 }, stricter)
305
306 // Loosen the package-level ratio AFTER proposal creation. If the
307 // executor read the package var, this would let the change slip
308 // through.
309 trustLevelRatio = trustRatio{numerator: 1, denominator: 3}
310
311 uassert.AbortsContains(t, errTrustLevelViolated, func() {
312 _ = exec.Execute(cross)
313 })
314}
315
316func TestNewValidatorProposalRequest_RejectsBaselineWipeViaUpsert(t *testing.T) {
317 // Regression test for the OLD-vs-NEW weights bug: a proposal that
318 // removes most baseline validators and inflates the remaining
319 // survivor's voting power must be rejected, because an IBC light
320 // client trusting the baseline would refuse to verify the
321 // resulting header (its tally uses baseline VPs, not the inflated
322 // new VP).
323 //
324 // Baseline: A:1, B:1, C:1 (prevTotal=3, threshold = 1 at 1/3,
325 // strict cross-multiply: pass iff retained*3 > 3, i.e. retained > 1).
326 // Proposal: remove A, remove B, upsert C → 100.
327 // Survivors: {C}. retained = C's BASELINE VP = 1.
328 // 1*3 = 3, 3*1 = 3, 3 <= 3 ⇒ aborts. ✓
329 //
330 // If the executor had used NEW weights (the previous bug), retained
331 // would be 100 (C's NEW VP), and the check would have passed —
332 // false-positive accept of a proposal that breaks IBC safety.
333 resetValset(t)
334 resetCache()
335 resetLimits()
336
337 testing.SetSysParamStrings(module, submodule, currKey, []string{
338 pubKeyA + ":1",
339 pubKeyB + ":1",
340 pubKeyC + ":1",
341 })
342 opA := testutils.TestAddress("op-A")
343 opB := testutils.TestAddress("op-B")
344 opC := testutils.TestAddress("op-C")
345 seedCache(t, []struct {
346 op address
347 pubKey string
348 keepRunning bool
349 }{
350 {op: opA, pubKey: pubKeyA, keepRunning: true},
351 {op: opB, pubKey: pubKeyB, keepRunning: true},
352 {op: opC, pubKey: pubKeyC, keepRunning: true},
353 })
354
355 exec := newValoperChangeExecutor([]ValoperChange{
356 {OperatorAddress: opA, Power: 0},
357 {OperatorAddress: opB, Power: 0},
358 {OperatorAddress: opC, Power: 100},
359 }, trustLevelRatio)
360
361 uassert.AbortsContains(t, errTrustLevelViolated, func() {
362 _ = exec.Execute(cross)
363 })
364}
365
366func TestNewTrustLevelPropRequest_UpdatesRatio(t *testing.T) {
367 resetLimits()
368
369 numBefore, denBefore := GetTrustLevel()
370 uassert.Equal(t, uint64(1), numBefore)
371 uassert.Equal(t, uint64(3), denBefore)
372
373 // NewTrustLevelPropRequest validates at create-time; the executor is
374 // re-built here to drive it directly (ProposalRequest doesn't expose
375 // its Executor). Going through dao.NewSimpleExecutor would duplicate
376 // the factory's branching, so use the private helper instead.
377 _ = NewTrustLevelPropRequest(2, 5, "tighten trust level", "require 40% legacy power")
378 exec := newTrustLevelExecutor(trustRatio{numerator: 2, denominator: 5})
379 urequire.NoError(t, exec.Execute(cross))
380
381 numAfter, denAfter := GetTrustLevel()
382 uassert.Equal(t, uint64(2), numAfter)
383 uassert.Equal(t, uint64(5), denAfter)
384}
385
386func TestNewTrustLevelPropRequest_RejectsOutOfBounds(t *testing.T) {
387 resetLimits()
388
389 // numerator/denominator must yield a ratio in [1/3, 2/3].
390 // Out-of-band overflow is caught at execute-time by overflow.Mulu64
391 // in the trust-level check, not at create-time — see
392 // TestNewValidatorProposalRequest_ExecutorRejectsRatioOverflow.
393 cases := []struct{ num, den uint64 }{
394 {0, 3}, // 0/3 < 1/3
395 {1, 4}, // 1/4 < 1/3
396 {3, 4}, // 3/4 > 2/3
397 {3, 3}, // 1/1 > 2/3
398 {1, 0}, // div-by-zero guard
399 }
400 for _, c := range cases {
401 uassert.PanicsContains(t, errInvalidTrustLevel, func() {
402 _ = NewTrustLevelPropRequest(c.num, c.den, "invalid", "")
403 })
404 }
405}
406
407func TestNewCooldownPropRequest_UpdatesCooldown(t *testing.T) {
408 resetLimits()
409
410 cdBefore := GetCooldown()
411 uassert.Equal(t, uint64(24*60*60), cdBefore) // 24h in seconds
412
413 // NewCooldownPropRequest validates at create-time; the executor is
414 // re-built here to drive it directly (ProposalRequest doesn't
415 // expose its Executor).
416 _ = NewCooldownPropRequest(0, "disable cooldown for testnet", "")
417 exec := newCooldownExecutor(0)
418 urequire.NoError(t, exec.Execute(cross))
419
420 cdAfter := GetCooldown()
421 uassert.Equal(t, uint64(0), cdAfter)
422}
423
424func TestNewCooldownPropRequest_RejectsOverflowingSeconds(t *testing.T) {
425 resetLimits()
426
427 // Any input above cooldownMaxSeconds is rejected at create-time.
428 // Without this guard, time.Duration(seconds) * time.Second
429 // silently overflows int64 past ~9.22e9 seconds (≈292 years) and
430 // could wrap to a tiny or negative duration, effectively
431 // disabling the cooldown.
432 cases := []uint64{
433 cooldownMaxSeconds + 1, // just above the cap
434 1 << 33, // ≈272 years, comfortably overflowing
435 ^uint64(0), // uint64 max
436 }
437 for _, s := range cases {
438 uassert.PanicsContains(t, errInvalidCooldown, func() {
439 _ = NewCooldownPropRequest(s, "too long", "")
440 })
441 }
442
443 // Cap itself is accepted.
444 urequire.NotPanics(t, func() {
445 _ = NewCooldownPropRequest(cooldownMaxSeconds, "at cap", "")
446 })
447}
448
449func TestNewCooldownPropRequest_AllowsBackToBackAfterDisable(t *testing.T) {
450 // Concrete test of the use case the proposal exists for: after a
451 // cooldown=0 proposal executes, subsequent valset proposals are
452 // no longer blocked by the cooldown (creation OR execution).
453 resetValset(t)
454 resetCache()
455 resetLimits()
456
457 // Pretend a valset update just happened.
458 lastValsetUpdate = time.Now()
459
460 // Disable the cooldown via governance.
461 urequire.NoError(t, newCooldownExecutor(0).Execute(cross))
462
463 // Now a back-to-back valset proposal must pass cooldown checks.
464 testing.SetSysParamStrings(module, submodule, currKey, []string{
465 pubKeyA + ":1",
466 pubKeyB + ":1",
467 })
468 opA := testutils.TestAddress("op-A")
469 opB := testutils.TestAddress("op-B")
470 seedCache(t, []struct {
471 op address
472 pubKey string
473 keepRunning bool
474 }{
475 {op: opA, pubKey: pubKeyA, keepRunning: true},
476 {op: opB, pubKey: pubKeyB, keepRunning: true},
477 })
478
479 // Create-time cooldown check uses live cooldown (now 0) — passes.
480 pr := NewValidatorProposalRequest(
481 []ValoperChange{{OperatorAddress: opA, Power: 5}},
482 "upsert opA after cooldown disabled", "",
483 )
484 _ = pr
485
486 // Execute path: same live check, also passes.
487 exec := newValoperChangeExecutor(
488 []ValoperChange{{OperatorAddress: opA, Power: 5}},
489 trustLevelRatio,
490 )
491 urequire.NoError(t, exec.Execute(cross))
492}
493
494func TestNewValidatorProposalRequest_ExecutorRejectsRatioOverflow(t *testing.T) {
495 // Defense-in-depth: a maliciously-crafted (but in-bounds) ratio
496 // like (2<<40, 3<<40) is accepted by NewTrustLevelPropRequest
497 // (ratio = 2/3, within [1/3, 2/3]). If a future baseline grows
498 // large enough that baselineTotal * numerator overflows uint64,
499 // overflow.Mulu64 in the trust-level check aborts rather than
500 // returning a silently-wrong comparison.
501 //
502 // To trigger overflow at execute-time we need baselineTotal *
503 // snapshotTrustLevel.numerator to exceed 2^64. With one validator
504 // at VP=2^40, total=2^40, and a snapshot ratio with num=2^40,
505 // the product is 2^80 — definite overflow.
506 resetValset(t)
507 resetCache()
508 resetLimits()
509
510 testing.SetSysParamStrings(module, submodule, currKey, []string{
511 pubKeyA + ":1099511627776", // 2^40
512 })
513 opA := testutils.TestAddress("op-A")
514 seedCache(t, []struct {
515 op address
516 pubKey string
517 keepRunning bool
518 }{{op: opA, pubKey: pubKeyA, keepRunning: true}})
519
520 // Build the executor with a ratio whose numerator and denominator
521 // are individually fine, but whose product against a 2^40 baseline
522 // overflows uint64.
523 bigButValid := trustRatio{numerator: 1 << 40, denominator: 3 << 40}
524 exec := newValoperChangeExecutor([]ValoperChange{
525 {OperatorAddress: opA, Power: 1 << 40},
526 }, bigButValid)
527
528 uassert.AbortsContains(t, errTrustLevelOverflow, func() {
529 _ = exec.Execute(cross)
530 })
531}