Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}