timelock_guardian_test.gno
31.67 Kb · 908 lines
1package timelock_guardian
2
3import (
4 "chain"
5 "strings"
6 "testing"
7
8 "gno.land/p/nt/testutils/v0"
9 "gno.land/p/nt/uassert/v0"
10)
11
12var (
13 owner = testutils.TestAddress("owner")
14 guardian = testutils.TestAddress("guardian")
15 attacker = testutils.TestAddress("attacker")
16 newOwner = testutils.TestAddress("newowner")
17)
18
19// resetState resets all package-level state between tests.
20func resetState() {
21 targets = make(map[string]*TargetConfig)
22 targetNames = []string{}
23 ownerTargets = make(map[address]int)
24 actions = make(map[string]*Action)
25 actionIDs = []string{}
26 pending = make(map[string][]string)
27 pendingTargets = []string{}
28 ptIndex = make(map[string]int)
29 nextID = 1
30}
31
32// heights advancing time: SkipHeights moves the clock 5s per height.
33func skipSeconds(sec int64) {
34 testing.SkipHeights((sec + 4) / 5)
35}
36
37func registerUpgrades(cur realm) string {
38 testing.SetRealm(testing.NewUserRealm(owner))
39 RegisterTarget(cross(cur), "upgrades", MinDelayFloor, guardian)
40 return "upgrades"
41}
42
43// ---------- RegisterTarget ----------
44
45func TestRegisterTarget_Basic(cur realm, t *testing.T) {
46 resetState()
47 testing.SetRealm(testing.NewUserRealm(owner))
48
49 RegisterTarget(cross(cur), "upgrades", 120, guardian)
50
51 tc := targets["upgrades"]
52 uassert.Equal(t, string(owner), string(tc.Owner))
53 uassert.Equal(t, string(guardian), string(tc.Guardian))
54 uassert.Equal(t, int64(120), tc.MinDelay)
55}
56
57func TestRegisterTarget_NoGuardianAllowed(cur realm, t *testing.T) {
58 resetState()
59 testing.SetRealm(testing.NewUserRealm(owner))
60
61 RegisterTarget(cross(cur), "plain", MinDelayFloor, "")
62 uassert.Equal(t, "", string(targets["plain"].Guardian))
63}
64
65func TestRegisterTarget_DuplicateRejected(cur realm, t *testing.T) {
66 resetState()
67 testing.SetRealm(testing.NewUserRealm(owner))
68 registerUpgrades(cur)
69
70 uassert.AbortsWithMessage(t, cur,
71 "target already registered: upgrades",
72 func() { RegisterTarget(cross(cur), "upgrades", MinDelayFloor, "") },
73 )
74}
75
76func TestRegisterTarget_InvalidNameRejected(cur realm, t *testing.T) {
77 resetState()
78 testing.SetRealm(testing.NewUserRealm(owner))
79
80 for _, bad := range []string{"", "Upgrades", "has space", strings.Repeat("a", MaxTargetNameLen+1)} {
81 uassert.AbortsContains(t, cur,
82 "target name must be",
83 func() { RegisterTarget(cross(cur), bad, MinDelayFloor, "") },
84 )
85 }
86}
87
88func TestRegisterTarget_DelayBoundsEnforced(cur realm, t *testing.T) {
89 resetState()
90 testing.SetRealm(testing.NewUserRealm(owner))
91
92 uassert.AbortsContains(t, cur,
93 "minimum delay must be at least",
94 func() { RegisterTarget(cross(cur), "too_fast", MinDelayFloor-1, "") },
95 )
96 uassert.AbortsContains(t, cur,
97 "minimum delay exceeds the maximum",
98 func() { RegisterTarget(cross(cur), "too_slow", MaxDelay+1, "") },
99 )
100}
101
102func TestRegisterTarget_InvalidGuardianRejected(cur realm, t *testing.T) {
103 resetState()
104 testing.SetRealm(testing.NewUserRealm(owner))
105
106 uassert.AbortsWithMessage(t, cur,
107 "invalid guardian address: junk",
108 func() { RegisterTarget(cross(cur), "upgrades", MinDelayFloor, address("junk")) },
109 )
110}
111
112// ---------- SetGuardian / two-step TransferTargetOwnership ----------
113
114func TestSetGuardian_OwnerOnly(cur realm, t *testing.T) {
115 resetState()
116 testing.SetRealm(testing.NewUserRealm(owner))
117 registerUpgrades(cur)
118
119 testing.SetRealm(testing.NewUserRealm(attacker))
120 uassert.AbortsWithMessage(t, cur,
121 "only the target owner can set the guardian",
122 func() { SetGuardian(cross(cur), "upgrades", attacker) },
123 )
124
125 testing.SetRealm(testing.NewUserRealm(owner))
126 SetGuardian(cross(cur), "upgrades", "")
127 uassert.Equal(t, "", string(targets["upgrades"].Guardian))
128}
129
130func TestTransferTargetOwnership_TwoStep(cur realm, t *testing.T) {
131 resetState()
132 testing.SetRealm(testing.NewUserRealm(owner))
133 registerUpgrades(cur)
134
135 testing.SetRealm(testing.NewUserRealm(attacker))
136 uassert.AbortsWithMessage(t, cur,
137 "only the target owner can transfer ownership",
138 func() { TransferTargetOwnership(cross(cur), "upgrades", attacker) },
139 )
140
141 testing.SetRealm(testing.NewUserRealm(owner))
142 uassert.AbortsWithMessage(t, cur,
143 "invalid new owner address: junk",
144 func() { TransferTargetOwnership(cross(cur), "upgrades", address("junk")) },
145 )
146
147 uassert.AbortsWithMessage(t, cur,
148 "new owner is already the owner",
149 func() { TransferTargetOwnership(cross(cur), "upgrades", owner) },
150 )
151
152 // the offer alone changes nothing
153 TransferTargetOwnership(cross(cur), "upgrades", newOwner)
154 uassert.Equal(t, string(owner), string(targets["upgrades"].Owner))
155 uassert.Equal(t, string(newOwner), string(targets["upgrades"].PendingOwner))
156 uassert.Equal(t, 1, ownerTargets[owner])
157 uassert.Equal(t, 0, ownerTargets[newOwner])
158
159 // only the nominee can accept
160 testing.SetRealm(testing.NewUserRealm(attacker))
161 uassert.AbortsWithMessage(t, cur,
162 "caller is not the pending owner",
163 func() { AcceptTargetOwnership(cross(cur), "upgrades") },
164 )
165
166 testing.SetRealm(testing.NewUserRealm(newOwner))
167 AcceptTargetOwnership(cross(cur), "upgrades")
168 uassert.Equal(t, string(newOwner), string(targets["upgrades"].Owner))
169 uassert.Equal(t, "", string(targets["upgrades"].PendingOwner))
170 uassert.Equal(t, 0, ownerTargets[owner])
171 uassert.Equal(t, 1, ownerTargets[newOwner])
172}
173
174func TestTransferTargetOwnership_ClearOffer(cur realm, t *testing.T) {
175 resetState()
176 testing.SetRealm(testing.NewUserRealm(owner))
177 registerUpgrades(cur)
178
179 TransferTargetOwnership(cross(cur), "upgrades", newOwner)
180 TransferTargetOwnership(cross(cur), "upgrades", "") // clear
181
182 testing.SetRealm(testing.NewUserRealm(newOwner))
183 uassert.AbortsWithMessage(t, cur,
184 "caller is not the pending owner",
185 func() { AcceptTargetOwnership(cross(cur), "upgrades") },
186 )
187}
188
189func TestTransferTargetOwnership_QuotaCheckedAtAccept(cur realm, t *testing.T) {
190 resetState()
191 testing.SetRealm(testing.NewUserRealm(newOwner))
192 for i := 0; i < MaxTargetsPerOwner; i++ {
193 RegisterTarget(cross(cur), "t_"+strings.Repeat("x", i+1), MinDelayFloor, "")
194 }
195
196 testing.SetRealm(testing.NewUserRealm(owner))
197 registerUpgrades(cur)
198 TransferTargetOwnership(cross(cur), "upgrades", newOwner) // offer succeeds
199
200 // ...but accepting past the quota is refused, and ownership is unchanged
201 testing.SetRealm(testing.NewUserRealm(newOwner))
202 uassert.AbortsWithMessage(t, cur,
203 "accepting would exceed the per-owner target limit",
204 func() { AcceptTargetOwnership(cross(cur), "upgrades") },
205 )
206 uassert.Equal(t, string(owner), string(targets["upgrades"].Owner))
207}
208
209func TestUnsolicitedOfferConsumesNothing(cur realm, t *testing.T) {
210 resetState()
211 victim := testutils.TestAddress("victim")
212
213 // fix Y5 regression: an attacker offering targets to a victim must
214 // not consume the victim's quota or change any of their state
215 testing.SetRealm(testing.NewUserRealm(attacker))
216 for i := 0; i < MaxTargetsPerOwner; i++ {
217 name := "junk_" + strings.Repeat("x", i+1)
218 RegisterTarget(cross(cur), name, MinDelayFloor, "")
219 TransferTargetOwnership(cross(cur), name, victim)
220 }
221 uassert.Equal(t, 0, ownerTargets[victim])
222
223 // the victim can still register a full quota of their own targets
224 testing.SetRealm(testing.NewUserRealm(victim))
225 for i := 0; i < MaxTargetsPerOwner; i++ {
226 RegisterTarget(cross(cur), "mine_"+strings.Repeat("y", i+1), MinDelayFloor, "")
227 }
228}
229
230// ---------- Schedule ----------
231
232func TestSchedule_OwnerOnly(cur realm, t *testing.T) {
233 resetState()
234 testing.SetRealm(testing.NewUserRealm(owner))
235 registerUpgrades(cur)
236
237 // an attacker cannot schedule an action against someone else's
238 // target — the core binding the audit found missing
239 testing.SetRealm(testing.NewUserRealm(attacker))
240 uassert.AbortsWithMessage(t, cur,
241 "only the target owner can schedule actions for upgrades",
242 func() { Schedule(cross(cur), "upgrades", "malicious upgrade", MinDelayFloor) },
243 )
244}
245
246func TestSchedule_UnregisteredTargetRejected(cur realm, t *testing.T) {
247 resetState()
248 testing.SetRealm(testing.NewUserRealm(owner))
249
250 uassert.AbortsWithMessage(t, cur,
251 "target not found: ghost",
252 func() { Schedule(cross(cur), "ghost", "data", MinDelayFloor) },
253 )
254}
255
256func TestSchedule_MinDelayEnforced(cur realm, t *testing.T) {
257 resetState()
258 testing.SetRealm(testing.NewUserRealm(owner))
259 RegisterTarget(cross(cur), "upgrades", 3600, "")
260
261 // a delay below the target's registered minimum is rejected — no
262 // more 1-second "timelocks"
263 uassert.AbortsWithMessage(t, cur,
264 "delay below the target's minimum of 3600 seconds",
265 func() { Schedule(cross(cur), "upgrades", "data", 60) },
266 )
267}
268
269func TestSchedule_MaxDelayEnforced(cur realm, t *testing.T) {
270 resetState()
271 testing.SetRealm(testing.NewUserRealm(owner))
272 registerUpgrades(cur)
273
274 // overflow regression: a delay past MaxDelay must abort, never wrap
275 // ExecuteAfter into the past
276 uassert.AbortsContains(t, cur,
277 "delay exceeds the maximum",
278 func() { Schedule(cross(cur), "upgrades", "data", MaxDelay+1) },
279 )
280}
281
282func TestSchedule_DataValidation(cur realm, t *testing.T) {
283 resetState()
284 testing.SetRealm(testing.NewUserRealm(owner))
285 registerUpgrades(cur)
286
287 uassert.AbortsWithMessage(t, cur,
288 "data must not be empty",
289 func() { Schedule(cross(cur), "upgrades", "", MinDelayFloor) },
290 )
291 uassert.AbortsContains(t, cur,
292 "data exceeds",
293 func() { Schedule(cross(cur), "upgrades", strings.Repeat("x", MaxDataLen+1), MinDelayFloor) },
294 )
295}
296
297func TestSchedule_PendingCap(cur realm, t *testing.T) {
298 resetState()
299 testing.SetRealm(testing.NewUserRealm(owner))
300 registerUpgrades(cur)
301
302 for i := 0; i < MaxPendingPerTarget; i++ {
303 Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
304 }
305 uassert.AbortsWithMessage(t, cur,
306 "too many pending actions for target upgrades",
307 func() { Schedule(cross(cur), "upgrades", "op", MinDelayFloor) },
308 )
309}
310
311func TestSchedule_SetsWindows(cur realm, t *testing.T) {
312 resetState()
313 testing.SetRealm(testing.NewUserRealm(owner))
314 registerUpgrades(cur)
315
316 id := Schedule(cross(cur), "upgrades", "rotate keys", 120)
317 a := actions[id]
318 uassert.Equal(t, int64(120), int64(a.ExecuteAfter.Sub(a.ScheduledAt).Seconds()))
319 uassert.Equal(t, GracePeriod, int64(a.ExpiresAt.Sub(a.ExecuteAfter).Seconds()))
320}
321
322// ---------- Execute ----------
323
324func TestExecute_TooEarlyBlocked(cur realm, t *testing.T) {
325 resetState()
326 testing.SetRealm(testing.NewUserRealm(owner))
327 registerUpgrades(cur)
328 id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
329
330 uassert.AbortsContains(t, cur, "too early", func() { Execute(cross(cur), id) })
331 uassert.False(t, IsReady(id), "must not be ready before the delay")
332}
333
334func TestExecute_ReadyAfterDelay_AnyoneCanExecute(cur realm, t *testing.T) {
335 resetState()
336 testing.SetRealm(testing.NewUserRealm(owner))
337 registerUpgrades(cur)
338 id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
339
340 skipSeconds(MinDelayFloor)
341 uassert.True(t, IsReady(id), "must be ready after the delay")
342
343 // executor identity is deliberately unrestricted
344 testing.SetRealm(testing.NewUserRealm(attacker))
345 uassert.Equal(t, "executed: "+id, Execute(cross(cur), id))
346 uassert.True(t, IsExecuted(id), "must record execution")
347}
348
349func TestExecute_TwiceBlocked(cur realm, t *testing.T) {
350 resetState()
351 testing.SetRealm(testing.NewUserRealm(owner))
352 registerUpgrades(cur)
353 id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
354 skipSeconds(MinDelayFloor)
355 Execute(cross(cur), id)
356
357 uassert.AbortsWithMessage(t, cur, "action already executed", func() { Execute(cross(cur), id) })
358}
359
360func TestExecute_ExpiredBlocked(cur realm, t *testing.T) {
361 resetState()
362 testing.SetRealm(testing.NewUserRealm(owner))
363 registerUpgrades(cur)
364 id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
365
366 // travel past the grace window: stale actions cannot be sprung later
367 skipSeconds(MinDelayFloor + GracePeriod + 10)
368 uassert.False(t, IsReady(id), "expired action must not be ready")
369 uassert.AbortsContains(t, cur, "action expired", func() { Execute(cross(cur), id) })
370 uassert.Equal(t, "none", GetPending())
371}
372
373// ---------- Cancel / Veto ----------
374
375func TestCancel_CurrentOwnerOnly(cur realm, t *testing.T) {
376 resetState()
377 testing.SetRealm(testing.NewUserRealm(owner))
378 registerUpgrades(cur)
379 id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
380
381 testing.SetRealm(testing.NewUserRealm(attacker))
382 uassert.AbortsWithMessage(t, cur,
383 "only the target owner can cancel",
384 func() { Cancel(cross(cur), id) },
385 )
386
387 testing.SetRealm(testing.NewUserRealm(owner))
388 uassert.Equal(t, "cancelled: "+id, Cancel(cross(cur), id))
389 _, present := actions[id]
390 uassert.False(t, present, "cancelled action must be reaped from state")
391
392 // re-audit P3: after an ownership transfer the SCHEDULING creator
393 // loses cancel rights; only the current owner holds them
394 id2 := Schedule(cross(cur), "upgrades", "op2", MinDelayFloor)
395 TransferTargetOwnership(cross(cur), "upgrades", newOwner)
396 testing.SetRealm(testing.NewUserRealm(newOwner))
397 AcceptTargetOwnership(cross(cur), "upgrades")
398 testing.SetRealm(testing.NewUserRealm(owner))
399 uassert.AbortsWithMessage(t, cur,
400 "only the target owner can cancel",
401 func() { Cancel(cross(cur), id2) },
402 )
403 testing.SetRealm(testing.NewUserRealm(newOwner))
404 uassert.Equal(t, "cancelled: "+id2, Cancel(cross(cur), id2))
405}
406
407func TestCancel_ExecutedBlocked(cur realm, t *testing.T) {
408 resetState()
409 testing.SetRealm(testing.NewUserRealm(owner))
410 registerUpgrades(cur)
411 id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
412 skipSeconds(MinDelayFloor)
413 Execute(cross(cur), id)
414
415 uassert.AbortsWithMessage(t, cur,
416 "cannot cancel an already-executed action",
417 func() { Cancel(cross(cur), id) },
418 )
419}
420
421func TestVeto_GuardianOnly(cur realm, t *testing.T) {
422 resetState()
423 testing.SetRealm(testing.NewUserRealm(owner))
424 registerUpgrades(cur)
425 id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
426
427 testing.SetRealm(testing.NewUserRealm(attacker))
428 uassert.AbortsWithMessage(t, cur,
429 "only the target guardian can veto",
430 func() { Veto(cross(cur), id) },
431 )
432
433 testing.SetRealm(testing.NewUserRealm(guardian))
434 uassert.Equal(t, "vetoed: "+id, Veto(cross(cur), id))
435 _, present := actions[id]
436 uassert.False(t, present, "vetoed action must be reaped from state")
437
438 // a vetoed action can never execute
439 skipSeconds(MinDelayFloor)
440 uassert.AbortsWithMessage(t, cur, "action not found: "+id, func() { Execute(cross(cur), id) })
441}
442
443func TestVeto_NoGuardianTargetBlocked(cur realm, t *testing.T) {
444 resetState()
445 testing.SetRealm(testing.NewUserRealm(owner))
446 RegisterTarget(cross(cur), "plain", MinDelayFloor, "")
447 id := Schedule(cross(cur), "plain", "op", MinDelayFloor)
448
449 testing.SetRealm(testing.NewUserRealm(guardian))
450 uassert.AbortsWithMessage(t, cur,
451 "target has no guardian: plain",
452 func() { Veto(cross(cur), id) },
453 )
454}
455
456// ---------- Expire (fix Y2) ----------
457
458func TestExpire_PermissionlessRecovery(cur realm, t *testing.T) {
459 resetState()
460 testing.SetRealm(testing.NewUserRealm(owner))
461 registerUpgrades(cur)
462
463 // fill the target's quota, then let everything expire
464 var ids []string
465 for i := 0; i < MaxPendingPerTarget; i++ {
466 ids = append(ids, Schedule(cross(cur), "upgrades", "op", MinDelayFloor))
467 }
468 skipSeconds(MinDelayFloor + GracePeriod + 10)
469
470 // fix Y2 regression: ANYONE — not just a lucky sweep window — can
471 // reap a provably expired action and free the slot it wedges
472 testing.SetRealm(testing.NewUserRealm(attacker))
473 uassert.Equal(t, "expired: "+ids[0], Expire(cross(cur), ids[0]))
474 _, present := actions[ids[0]]
475 uassert.False(t, present, "expired action must be reaped from state")
476
477 // reap the rest as the owner; quota and SetGuardian both recover
478 testing.SetRealm(testing.NewUserRealm(owner))
479 for _, id := range ids[1:] {
480 Expire(cross(cur), id)
481 }
482 uassert.Equal(t, 0, pendingCount("upgrades"))
483 SetGuardian(cross(cur), "upgrades", "") // no longer blocked
484 Schedule(cross(cur), "upgrades", "fresh", MinDelayFloor)
485}
486
487func TestExpire_RefusesLiveAndExecuted(cur realm, t *testing.T) {
488 resetState()
489 testing.SetRealm(testing.NewUserRealm(owner))
490 registerUpgrades(cur)
491 live := Schedule(cross(cur), "upgrades", "live", MinDelayFloor)
492 done := Schedule(cross(cur), "upgrades", "done", MinDelayFloor)
493 skipSeconds(MinDelayFloor)
494 Execute(cross(cur), done)
495
496 uassert.AbortsWithMessage(t, cur, "action is not expired",
497 func() { Expire(cross(cur), live) })
498 uassert.AbortsWithMessage(t, cur, "action already executed",
499 func() { Expire(cross(cur), done) })
500 uassert.AbortsWithMessage(t, cur, "action not found: ghost",
501 func() { Expire(cross(cur), "ghost") })
502}
503
504// ---------- stray sends (fix Y6) ----------
505
506func TestStraySendRejectedEverywhere(cur realm, t *testing.T) {
507 resetState()
508 testing.SetRealm(testing.NewUserRealm(owner))
509 registerUpgrades(cur)
510 id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
511
512 // fix Y6 regression: this realm holds no banker, so an attached
513 // send would strand on the realm address forever — every crossing
514 // function must abort it (the abort reverts the transfer)
515 msg := "this realm does not accept coins"
516 testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}})
517 uassert.AbortsWithMessage(t, cur, msg, func() { RegisterTarget(cross(cur), "paid", MinDelayFloor, "") })
518 uassert.AbortsWithMessage(t, cur, msg, func() { Schedule(cross(cur), "upgrades", "op2", MinDelayFloor) })
519 uassert.AbortsWithMessage(t, cur, msg, func() { Cancel(cross(cur), id) })
520 uassert.AbortsWithMessage(t, cur, msg, func() { SetGuardian(cross(cur), "upgrades", "") })
521 uassert.AbortsWithMessage(t, cur, msg, func() { TransferTargetOwnership(cross(cur), "upgrades", newOwner) })
522 uassert.AbortsWithMessage(t, cur, msg, func() { AcceptTargetOwnership(cross(cur), "upgrades") })
523 uassert.AbortsWithMessage(t, cur, msg, func() { Veto(cross(cur), id) })
524 uassert.AbortsWithMessage(t, cur, msg, func() { Expire(cross(cur), id) })
525 // SkipHeights resets the test context (wiping the send envelope),
526 // so re-arm it before probing the ready-action Execute path
527 skipSeconds(MinDelayFloor)
528 testing.SetRealm(testing.NewUserRealm(owner))
529 testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}})
530 uassert.AbortsWithMessage(t, cur, msg, func() { Execute(cross(cur), id) })
531 testing.SetOriginSend(nil)
532
533 // without coins the same call succeeds
534 uassert.Equal(t, "executed: "+id, Execute(cross(cur), id))
535}
536
537// ---------- queries + render ----------
538
539func TestGetPending_ExcludesFinished(cur realm, t *testing.T) {
540 resetState()
541 testing.SetRealm(testing.NewUserRealm(owner))
542 registerUpgrades(cur)
543 id1 := Schedule(cross(cur), "upgrades", "op1", MinDelayFloor)
544 id2 := Schedule(cross(cur), "upgrades", "op2", MinDelayFloor)
545 id3 := Schedule(cross(cur), "upgrades", "op3", MinDelayFloor)
546
547 Cancel(cross(cur), id2)
548 skipSeconds(MinDelayFloor)
549 Execute(cross(cur), id1)
550
551 uassert.Equal(t, id3, GetPending())
552}
553
554func TestGetPending_GroupedByTarget(cur realm, t *testing.T) {
555 resetState()
556 testing.SetRealm(testing.NewUserRealm(owner))
557 RegisterTarget(cross(cur), "alpha", MinDelayFloor, "")
558 RegisterTarget(cross(cur), "beta", MinDelayFloor, "")
559
560 // interleave scheduling across targets; output groups by target in
561 // registration order (fix Y4 bookkeeping), insertion-ordered within
562 a1 := Schedule(cross(cur), "alpha", "a1", MinDelayFloor)
563 b1 := Schedule(cross(cur), "beta", "b1", MinDelayFloor)
564 a2 := Schedule(cross(cur), "alpha", "a2", MinDelayFloor)
565
566 uassert.Equal(t, a1+", "+a2+", "+b1, GetPending())
567 uassert.Equal(t, 2, pendingCount("alpha"))
568 uassert.Equal(t, 1, pendingCount("beta"))
569
570 Cancel(cross(cur), a1)
571 uassert.Equal(t, a2+", "+b1, GetPending())
572 uassert.Equal(t, 1, pendingCount("alpha"))
573}
574
575func TestGetTarget_Summary(cur realm, t *testing.T) {
576 resetState()
577 testing.SetRealm(testing.NewUserRealm(owner))
578 registerUpgrades(cur)
579
580 out := GetTarget("upgrades")
581 uassert.True(t, strings.Contains(out, string(owner)), "must show owner")
582 uassert.True(t, strings.Contains(out, string(guardian)), "must show guardian")
583 uassert.True(t, strings.Contains(out, "Min delay: 60s"), "must show min delay")
584
585 TransferTargetOwnership(cross(cur), "upgrades", newOwner)
586 uassert.True(t, strings.Contains(GetTarget("upgrades"), "Pending owner: "+string(newOwner)),
587 "must show a pending ownership offer")
588}
589
590func TestRender_SanitizesInjection(cur realm, t *testing.T) {
591 resetState()
592 testing.SetRealm(testing.NewUserRealm(owner))
593 registerUpgrades(cur)
594 Schedule(cross(cur), "upgrades", "evil` | injected\nrow", MinDelayFloor)
595
596 out := Render("")
597 uassert.False(t, strings.Contains(out, "evil`"), "backticks must be sanitized")
598 uassert.False(t, strings.Contains(out, "injected\nrow"), "newlines must be sanitized")
599}
600
601func TestRender_SanitizesHTMLBrackets(cur realm, t *testing.T) {
602 resetState()
603 testing.SetRealm(testing.NewUserRealm(owner))
604 registerUpgrades(cur)
605 // fix Y3 regression: raw HTML must never reach the page
606 Schedule(cross(cur), "upgrades", "<script>alert(1)</script>", MinDelayFloor)
607
608 out := Render("")
609 uassert.False(t, strings.Contains(out, "<script"), "script tags must be sanitized in Render")
610 uassert.False(t, strings.Contains(out, "</script"), "closing tags must be sanitized in Render")
611 one := GetAction(GetPending())
612 uassert.False(t, strings.Contains(one, "<script"), "script tags must be sanitized in GetAction")
613}
614
615func TestRender_EmptyState(cur realm, t *testing.T) {
616 resetState()
617 uassert.Equal(t, "# Timelock Guardian\n\nNo actions scheduled.\n", Render(""))
618}
619
620func TestRender_Sections(cur realm, t *testing.T) {
621 resetState()
622 testing.SetRealm(testing.NewUserRealm(owner))
623 registerUpgrades(cur)
624 id1 := Schedule(cross(cur), "upgrades", "op1", MinDelayFloor)
625 id2 := Schedule(cross(cur), "upgrades", "op2", MinDelayFloor)
626 Schedule(cross(cur), "upgrades", "op3", MinDelayFloor)
627 skipSeconds(MinDelayFloor)
628 Execute(cross(cur), id1)
629 Cancel(cross(cur), id2)
630
631 out := Render("")
632 uassert.True(t, strings.Contains(out, "## Executed (1)"), "executed section")
633 uassert.True(t, strings.Contains(out, "## Pending (1)"), "pending section")
634 uassert.False(t, strings.Contains(out, "Cancelled"), "cancelled actions are reaped, not rendered")
635}
636
637// ---------- re-audit regressions (2026-09-02) ----------
638
639func TestScheduleCancelCycling_CannotBrickTheRealm(cur realm, t *testing.T) {
640 resetState()
641 testing.SetRealm(testing.NewUserRealm(owner))
642 registerUpgrades(cur)
643
644 // re-audit P1: the old lifetime cap could be consumed forever by
645 // schedule+cancel cycling; the cap now bounds LIVE actions and
646 // cancelled records are reaped, so heavy cycling leaves no residue
647 for i := 0; i < 3*MaxPendingPerTarget; i++ {
648 id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
649 Cancel(cross(cur), id)
650 }
651 uassert.Equal(t, 0, len(actions), "cycling must leave no state residue")
652 uassert.Equal(t, 0, pendingCount("upgrades"), "no live actions after cycling")
653 Schedule(cross(cur), "upgrades", "op", MinDelayFloor) // must not abort
654}
655
656func TestSetGuardian_BlockedWhilePending(cur realm, t *testing.T) {
657 resetState()
658 testing.SetRealm(testing.NewUserRealm(owner))
659 registerUpgrades(cur)
660 id := Schedule(cross(cur), "upgrades", "rug", MinDelayFloor)
661
662 // re-audit P2: the owner must not be able to strip the guardian
663 // during the delay window the guardian exists to police
664 uassert.AbortsWithMessage(t, cur,
665 "cannot change the guardian while actions are pending",
666 func() { SetGuardian(cross(cur), "upgrades", "") },
667 )
668
669 // once nothing is pending, the owner may change the guardian
670 Cancel(cross(cur), id)
671 SetGuardian(cross(cur), "upgrades", "")
672 uassert.Equal(t, "", string(targets["upgrades"].Guardian))
673}
674
675func TestVetoAndCancel_RefuseExpired(cur realm, t *testing.T) {
676 resetState()
677 testing.SetRealm(testing.NewUserRealm(owner))
678 registerUpgrades(cur)
679 id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
680 skipSeconds(MinDelayFloor + GracePeriod + 10)
681
682 // re-audit P3: terminal-state writes on expired actions rewrote
683 // history; both must refuse and point at the Expire valve
684 uassert.AbortsWithMessage(t, cur, "action expired: nothing to cancel (use Expire)",
685 func() { Cancel(cross(cur), id) })
686 testing.SetRealm(testing.NewUserRealm(guardian))
687 uassert.AbortsWithMessage(t, cur, "action expired: nothing to veto (use Expire)",
688 func() { Veto(cross(cur), id) })
689}
690
691func TestExpiredActions_ReapedOnSchedule(cur realm, t *testing.T) {
692 resetState()
693 testing.SetRealm(testing.NewUserRealm(owner))
694 registerUpgrades(cur)
695 Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
696 skipSeconds(MinDelayFloor + GracePeriod + 10)
697
698 // the next Schedule sweeps expired actions out of state
699 Schedule(cross(cur), "upgrades", "op2", MinDelayFloor)
700 uassert.Equal(t, 1, len(actions), "expired action must be reaped by the sweep")
701}
702
703func TestRender_LinkInjectionSanitized(cur realm, t *testing.T) {
704 resetState()
705 testing.SetRealm(testing.NewUserRealm(owner))
706 registerUpgrades(cur)
707 Schedule(cross(cur), "upgrades", "[approve](https://evil.example)", MinDelayFloor)
708
709 out := Render("")
710 uassert.False(t, strings.Contains(out, "[approve]"), "link syntax must be sanitized")
711}
712
713func TestPerOwnerTargetCap(cur realm, t *testing.T) {
714 resetState()
715 testing.SetRealm(testing.NewUserRealm(owner))
716 for i := 0; i < MaxTargetsPerOwner; i++ {
717 RegisterTarget(cross(cur), "t_"+strings.Repeat("x", i+1), MinDelayFloor, "")
718 }
719 uassert.AbortsWithMessage(t, cur,
720 "per-owner target limit reached",
721 func() { RegisterTarget(cross(cur), "one_more", MinDelayFloor, "") },
722 )
723}
724
725// ---------- round-3 audit regressions (2026-09-02) ----------
726
727func TestSybilsCannotExhaustSharedCapacity(cur realm, t *testing.T) {
728 resetState()
729
730 // round-3 P1: two attackers filling their OWN per-owner quotas must
731 // never block a third owner's Schedule — there is no shared cap left
732 // to exhaust
733 sybils := []address{testutils.TestAddress("sybil_a"), testutils.TestAddress("sybil_b")}
734 for _, syb := range sybils {
735 testing.SetRealm(testing.NewUserRealm(syb))
736 for i := 0; i < MaxTargetsPerOwner; i++ {
737 name := "t_" + string(syb[len(syb)-4:]) + "_" + strings.Repeat("x", i+1)
738 RegisterTarget(cross(cur), name, MinDelayFloor, "")
739 for j := 0; j < MaxPendingPerTarget; j++ {
740 Schedule(cross(cur), name, "spam", MaxDelay) // 10y delay: sweep reclaims nothing
741 }
742 }
743 }
744
745 // a legitimate owner is entirely unaffected
746 testing.SetRealm(testing.NewUserRealm(owner))
747 RegisterTarget(cross(cur), "legit", MinDelayFloor, "")
748 Schedule(cross(cur), "legit", "real work", MinDelayFloor) // must not abort
749}
750
751func TestVictimRecoversBehindSybilWall(cur realm, t *testing.T) {
752 resetState()
753
754 // fix Y2/Y4 regression — the exact starvation case the old global
755 // budget-windowed sweep could not recover from: a wall of long-delay
756 // sybil actions in front, a victim whose actions expired behind it.
757 // Per-target sweeps and permissionless Expire make recovery local.
758 syb := testutils.TestAddress("wall")
759 testing.SetRealm(testing.NewUserRealm(syb))
760 for i := 0; i < 5; i++ {
761 name := "wall_" + strings.Repeat("w", i+1)
762 RegisterTarget(cross(cur), name, MinDelayFloor, "")
763 for j := 0; j < MaxPendingPerTarget; j++ {
764 Schedule(cross(cur), name, "wall", MaxDelay) // 100 live 10y entries
765 }
766 }
767
768 victim := testutils.TestAddress("victim2")
769 testing.SetRealm(testing.NewUserRealm(victim))
770 RegisterTarget(cross(cur), "vt", MinDelayFloor, guardian)
771 var ids []string
772 for j := 0; j < MaxPendingPerTarget; j++ {
773 ids = append(ids, Schedule(cross(cur), "vt", "op", MinDelayFloor))
774 }
775 skipSeconds(MinDelayFloor + GracePeriod + 10) // all 20 expire
776
777 // the victim's own next Schedule sweeps THEIR target despite the wall
778 Schedule(cross(cur), "vt", "recovered", MinDelayFloor)
779 uassert.Equal(t, 1, pendingCount("vt"), "expired actions swept despite the sybil wall")
780
781 // and Expire frees a wedged slot directly, from any key
782 id2 := Schedule(cross(cur), "vt", "will expire", MinDelayFloor)
783 testing.SetRealm(testing.NewUserRealm(attacker))
784 skipSeconds(MinDelayFloor + GracePeriod + 10)
785 Expire(cross(cur), id2)
786 _, present := actions[id2]
787 uassert.False(t, present, "Expire must reap regardless of global queue position")
788}
789
790func TestPendingCounterConsistency(cur realm, t *testing.T) {
791 resetState()
792 testing.SetRealm(testing.NewUserRealm(owner))
793 registerUpgrades(cur)
794
795 id1 := Schedule(cross(cur), "upgrades", "a", MinDelayFloor)
796 id2 := Schedule(cross(cur), "upgrades", "b", MinDelayFloor)
797 Schedule(cross(cur), "upgrades", "c", MinDelayFloor)
798 uassert.Equal(t, 3, pendingCount("upgrades"))
799
800 Cancel(cross(cur), id1)
801 uassert.Equal(t, 2, pendingCount("upgrades"))
802 skipSeconds(MinDelayFloor)
803 Execute(cross(cur), id2)
804 uassert.Equal(t, 1, pendingCount("upgrades"))
805
806 // expiry: the count drops when the sweep reaps
807 skipSeconds(GracePeriod + 10)
808 Schedule(cross(cur), "upgrades", "d", MinDelayFloor) // triggers sweep of expired "c"
809 uassert.Equal(t, 1, pendingCount("upgrades"))
810}
811
812func TestSanitize_EmphasisMarkers(cur realm, t *testing.T) {
813 resetState()
814 testing.SetRealm(testing.NewUserRealm(owner))
815 registerUpgrades(cur)
816 Schedule(cross(cur), "upgrades", "**ready** _now_", MinDelayFloor)
817
818 out := Render("")
819 uassert.False(t, strings.Contains(out, "**ready** _now_"),
820 "emphasis markers in Data must be sanitized")
821}
822
823// ---------- render-index bounds (fix Y4) ----------
824
825func TestRenderIndexSelfTrims(cur realm, t *testing.T) {
826 resetState()
827 testing.SetRealm(testing.NewUserRealm(owner))
828 RegisterTarget(cross(cur), "churn", MinDelayFloor, "")
829
830 // execute many actions so actionIDs would grow unboundedly without
831 // the trim; the index must stay within RenderIndexCap and old
832 // executed records must stay queryable via the map reads
833 var first string
834 for i := 0; i < RenderIndexCap+30; i++ {
835 id := Schedule(cross(cur), "churn", "op", MinDelayFloor)
836 if i == 0 {
837 first = id
838 }
839 skipSeconds(MinDelayFloor)
840 Execute(cross(cur), id)
841 }
842 uassert.True(t, len(actionIDs) <= RenderIndexCap, "render index must self-trim")
843 uassert.True(t, IsExecuted(first), "trimmed records stay queryable forever")
844 uassert.True(t, strings.Contains(GetAction(first), "Status: executed"),
845 "GetAction must still serve trimmed records")
846}
847
848// ---------- round-2 audit regressions ----------
849
850func TestSetGuardian_SweepsExpiredFirst(cur realm, t *testing.T) {
851 resetState()
852 testing.SetRealm(testing.NewUserRealm(owner))
853 registerUpgrades(cur)
854 Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
855 skipSeconds(MinDelayFloor + GracePeriod + 10)
856
857 // round-2 hardening: expired-but-unswept actions protect nothing
858 // and must not block a guardian rotation
859 SetGuardian(cross(cur), "upgrades", "")
860 uassert.Equal(t, "", string(targets["upgrades"].Guardian))
861 uassert.Equal(t, 0, pendingCount("upgrades"), "the gate sweep must reap expired actions")
862}
863
864func TestRenderShowsPendingDespiteChurn(cur realm, t *testing.T) {
865 resetState()
866 testing.SetRealm(testing.NewUserRealm(owner))
867 RegisterTarget(cross(cur), "vis", MinDelayFloor, "")
868 watched := Schedule(cross(cur), "vis", "watched rug", 2*(MinDelayFloor+5*(RenderIndexCap+30)))
869
870 // round-2 fix Y-2: flood the render index with executed churn; the
871 // live pending action must remain visible for its whole window
872 RegisterTarget(cross(cur), "churn2", MinDelayFloor, "")
873 for i := 0; i < RenderIndexCap+30; i++ {
874 id := Schedule(cross(cur), "churn2", "noise", MinDelayFloor)
875 skipSeconds(MinDelayFloor)
876 Execute(cross(cur), id)
877 }
878
879 out := Render("")
880 uassert.True(t, strings.Contains(out, watched), "a live pending action must survive render-index churn")
881 uassert.True(t, strings.Contains(out, "## Pending (1)"), "pending section from live lists")
882
883 // and state stays consistent for an action trimmed out of the
884 // render index: Cancel still works (absent-id removeID is a no-op)
885 uassert.Equal(t, "cancelled: "+watched, Cancel(cross(cur), watched))
886 uassert.Equal(t, 0, pendingCount("vis"))
887}
888
889func TestPendingTargetsBookkeeping(cur realm, t *testing.T) {
890 resetState()
891 testing.SetRealm(testing.NewUserRealm(owner))
892 RegisterTarget(cross(cur), "one", MinDelayFloor, "")
893 RegisterTarget(cross(cur), "two", MinDelayFloor, "")
894
895 // round-2 fix Y-1: only targets with live pendings are tracked
896 uassert.Equal(t, 0, len(pendingTargets))
897 a1 := Schedule(cross(cur), "one", "a", MinDelayFloor)
898 Schedule(cross(cur), "two", "b", MinDelayFloor)
899 uassert.Equal(t, 2, len(pendingTargets))
900
901 Cancel(cross(cur), a1)
902 uassert.Equal(t, 1, len(pendingTargets), "emptied target leaves the with-pendings list")
903 uassert.Equal(t, 1, len(ptIndex))
904
905 // registration alone never inflates the list
906 RegisterTarget(cross(cur), "three", MinDelayFloor, "")
907 uassert.Equal(t, 1, len(pendingTargets))
908}