Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}