1package timelock_guardian23import (4 "chain"5 "strings"6 "testing"78 "gno.land/p/nt/testutils/v0"9 "gno.land/p/nt/uassert/v0"10)1112var (13 owner = testutils.TestAddress("owner")14 guardian = testutils.TestAddress("guardian")15 attacker = testutils.TestAddress("attacker")16 newOwner = testutils.TestAddress("newowner")17)1819// 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 = 130}3132// heights advancing time: SkipHeights moves the clock 5s per height.33func skipSeconds(sec int64) {34 testing.SkipHeights((sec + 4) / 5)35}3637func registerUpgrades(cur realm) string {38 testing.SetRealm(testing.NewUserRealm(owner))39 RegisterTarget(cross(cur), "upgrades", MinDelayFloor, guardian)40 return "upgrades"41}4243// ---------- RegisterTarget ----------4445func TestRegisterTarget_Basic(cur realm, t *testing.T) {46 resetState()47 testing.SetRealm(testing.NewUserRealm(owner))4849 RegisterTarget(cross(cur), "upgrades", 120, guardian)5051 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}5657func TestRegisterTarget_NoGuardianAllowed(cur realm, t *testing.T) {58 resetState()59 testing.SetRealm(testing.NewUserRealm(owner))6061 RegisterTarget(cross(cur), "plain", MinDelayFloor, "")62 uassert.Equal(t, "", string(targets["plain"].Guardian))63}6465func TestRegisterTarget_DuplicateRejected(cur realm, t *testing.T) {66 resetState()67 testing.SetRealm(testing.NewUserRealm(owner))68 registerUpgrades(cur)6970 uassert.AbortsWithMessage(t, cur,71 "target already registered: upgrades",72 func() { RegisterTarget(cross(cur), "upgrades", MinDelayFloor, "") },73 )74}7576func TestRegisterTarget_InvalidNameRejected(cur realm, t *testing.T) {77 resetState()78 testing.SetRealm(testing.NewUserRealm(owner))7980 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}8788func TestRegisterTarget_DelayBoundsEnforced(cur realm, t *testing.T) {89 resetState()90 testing.SetRealm(testing.NewUserRealm(owner))9192 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}101102func TestRegisterTarget_InvalidGuardianRejected(cur realm, t *testing.T) {103 resetState()104 testing.SetRealm(testing.NewUserRealm(owner))105106 uassert.AbortsWithMessage(t, cur,107 "invalid guardian address: junk",108 func() { RegisterTarget(cross(cur), "upgrades", MinDelayFloor, address("junk")) },109 )110}111112// ---------- SetGuardian / two-step TransferTargetOwnership ----------113114func TestSetGuardian_OwnerOnly(cur realm, t *testing.T) {115 resetState()116 testing.SetRealm(testing.NewUserRealm(owner))117 registerUpgrades(cur)118119 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 )124125 testing.SetRealm(testing.NewUserRealm(owner))126 SetGuardian(cross(cur), "upgrades", "")127 uassert.Equal(t, "", string(targets["upgrades"].Guardian))128}129130func TestTransferTargetOwnership_TwoStep(cur realm, t *testing.T) {131 resetState()132 testing.SetRealm(testing.NewUserRealm(owner))133 registerUpgrades(cur)134135 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 )140141 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 )146147 uassert.AbortsWithMessage(t, cur,148 "new owner is already the owner",149 func() { TransferTargetOwnership(cross(cur), "upgrades", owner) },150 )151152 // the offer alone changes nothing153 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])158159 // only the nominee can accept160 testing.SetRealm(testing.NewUserRealm(attacker))161 uassert.AbortsWithMessage(t, cur,162 "caller is not the pending owner",163 func() { AcceptTargetOwnership(cross(cur), "upgrades") },164 )165166 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}173174func TestTransferTargetOwnership_ClearOffer(cur realm, t *testing.T) {175 resetState()176 testing.SetRealm(testing.NewUserRealm(owner))177 registerUpgrades(cur)178179 TransferTargetOwnership(cross(cur), "upgrades", newOwner)180 TransferTargetOwnership(cross(cur), "upgrades", "") // clear181182 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}188189func 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 }195196 testing.SetRealm(testing.NewUserRealm(owner))197 registerUpgrades(cur)198 TransferTargetOwnership(cross(cur), "upgrades", newOwner) // offer succeeds199200 // ...but accepting past the quota is refused, and ownership is unchanged201 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}208209func TestUnsolicitedOfferConsumesNothing(cur realm, t *testing.T) {210 resetState()211 victim := testutils.TestAddress("victim")212213 // fix Y5 regression: an attacker offering targets to a victim must214 // not consume the victim's quota or change any of their state215 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])222223 // the victim can still register a full quota of their own targets224 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}229230// ---------- Schedule ----------231232func TestSchedule_OwnerOnly(cur realm, t *testing.T) {233 resetState()234 testing.SetRealm(testing.NewUserRealm(owner))235 registerUpgrades(cur)236237 // an attacker cannot schedule an action against someone else's238 // target — the core binding the audit found missing239 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}245246func TestSchedule_UnregisteredTargetRejected(cur realm, t *testing.T) {247 resetState()248 testing.SetRealm(testing.NewUserRealm(owner))249250 uassert.AbortsWithMessage(t, cur,251 "target not found: ghost",252 func() { Schedule(cross(cur), "ghost", "data", MinDelayFloor) },253 )254}255256func TestSchedule_MinDelayEnforced(cur realm, t *testing.T) {257 resetState()258 testing.SetRealm(testing.NewUserRealm(owner))259 RegisterTarget(cross(cur), "upgrades", 3600, "")260261 // a delay below the target's registered minimum is rejected — no262 // 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}268269func TestSchedule_MaxDelayEnforced(cur realm, t *testing.T) {270 resetState()271 testing.SetRealm(testing.NewUserRealm(owner))272 registerUpgrades(cur)273274 // overflow regression: a delay past MaxDelay must abort, never wrap275 // ExecuteAfter into the past276 uassert.AbortsContains(t, cur,277 "delay exceeds the maximum",278 func() { Schedule(cross(cur), "upgrades", "data", MaxDelay+1) },279 )280}281282func TestSchedule_DataValidation(cur realm, t *testing.T) {283 resetState()284 testing.SetRealm(testing.NewUserRealm(owner))285 registerUpgrades(cur)286287 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}296297func TestSchedule_PendingCap(cur realm, t *testing.T) {298 resetState()299 testing.SetRealm(testing.NewUserRealm(owner))300 registerUpgrades(cur)301302 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}310311func TestSchedule_SetsWindows(cur realm, t *testing.T) {312 resetState()313 testing.SetRealm(testing.NewUserRealm(owner))314 registerUpgrades(cur)315316 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}321322// ---------- Execute ----------323324func 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)329330 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}333334func 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)339340 skipSeconds(MinDelayFloor)341 uassert.True(t, IsReady(id), "must be ready after the delay")342343 // executor identity is deliberately unrestricted344 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}348349func 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)356357 uassert.AbortsWithMessage(t, cur, "action already executed", func() { Execute(cross(cur), id) })358}359360func 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)365366 // travel past the grace window: stale actions cannot be sprung later367 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}372373// ---------- Cancel / Veto ----------374375func 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)380381 testing.SetRealm(testing.NewUserRealm(attacker))382 uassert.AbortsWithMessage(t, cur,383 "only the target owner can cancel",384 func() { Cancel(cross(cur), id) },385 )386387 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")391392 // re-audit P3: after an ownership transfer the SCHEDULING creator393 // loses cancel rights; only the current owner holds them394 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}406407func 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)414415 uassert.AbortsWithMessage(t, cur,416 "cannot cancel an already-executed action",417 func() { Cancel(cross(cur), id) },418 )419}420421func 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)426427 testing.SetRealm(testing.NewUserRealm(attacker))428 uassert.AbortsWithMessage(t, cur,429 "only the target guardian can veto",430 func() { Veto(cross(cur), id) },431 )432433 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")437438 // a vetoed action can never execute439 skipSeconds(MinDelayFloor)440 uassert.AbortsWithMessage(t, cur, "action not found: "+id, func() { Execute(cross(cur), id) })441}442443func 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)448449 testing.SetRealm(testing.NewUserRealm(guardian))450 uassert.AbortsWithMessage(t, cur,451 "target has no guardian: plain",452 func() { Veto(cross(cur), id) },453 )454}455456// ---------- Expire (fix Y2) ----------457458func TestExpire_PermissionlessRecovery(cur realm, t *testing.T) {459 resetState()460 testing.SetRealm(testing.NewUserRealm(owner))461 registerUpgrades(cur)462463 // fill the target's quota, then let everything expire464 var ids []string465 for i := 0; i < MaxPendingPerTarget; i++ {466 ids = append(ids, Schedule(cross(cur), "upgrades", "op", MinDelayFloor))467 }468 skipSeconds(MinDelayFloor + GracePeriod + 10)469470 // fix Y2 regression: ANYONE — not just a lucky sweep window — can471 // reap a provably expired action and free the slot it wedges472 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")476477 // reap the rest as the owner; quota and SetGuardian both recover478 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 blocked484 Schedule(cross(cur), "upgrades", "fresh", MinDelayFloor)485}486487func 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)495496 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}503504// ---------- stray sends (fix Y6) ----------505506func 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)511512 // fix Y6 regression: this realm holds no banker, so an attached513 // send would strand on the realm address forever — every crossing514 // 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 path527 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)532533 // without coins the same call succeeds534 uassert.Equal(t, "executed: "+id, Execute(cross(cur), id))535}536537// ---------- queries + render ----------538539func 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)546547 Cancel(cross(cur), id2)548 skipSeconds(MinDelayFloor)549 Execute(cross(cur), id1)550551 uassert.Equal(t, id3, GetPending())552}553554func 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, "")559560 // interleave scheduling across targets; output groups by target in561 // registration order (fix Y4 bookkeeping), insertion-ordered within562 a1 := Schedule(cross(cur), "alpha", "a1", MinDelayFloor)563 b1 := Schedule(cross(cur), "beta", "b1", MinDelayFloor)564 a2 := Schedule(cross(cur), "alpha", "a2", MinDelayFloor)565566 uassert.Equal(t, a1+", "+a2+", "+b1, GetPending())567 uassert.Equal(t, 2, pendingCount("alpha"))568 uassert.Equal(t, 1, pendingCount("beta"))569570 Cancel(cross(cur), a1)571 uassert.Equal(t, a2+", "+b1, GetPending())572 uassert.Equal(t, 1, pendingCount("alpha"))573}574575func TestGetTarget_Summary(cur realm, t *testing.T) {576 resetState()577 testing.SetRealm(testing.NewUserRealm(owner))578 registerUpgrades(cur)579580 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")584585 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}589590func 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)595596 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}600601func 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 page606 Schedule(cross(cur), "upgrades", "<script>alert(1)</script>", MinDelayFloor)607608 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}614615func TestRender_EmptyState(cur realm, t *testing.T) {616 resetState()617 uassert.Equal(t, "# Timelock Guardian\n\nNo actions scheduled.\n", Render(""))618}619620func 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)630631 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}636637// ---------- re-audit regressions (2026-09-02) ----------638639func TestScheduleCancelCycling_CannotBrickTheRealm(cur realm, t *testing.T) {640 resetState()641 testing.SetRealm(testing.NewUserRealm(owner))642 registerUpgrades(cur)643644 // re-audit P1: the old lifetime cap could be consumed forever by645 // schedule+cancel cycling; the cap now bounds LIVE actions and646 // cancelled records are reaped, so heavy cycling leaves no residue647 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 abort654}655656func 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)661662 // re-audit P2: the owner must not be able to strip the guardian663 // during the delay window the guardian exists to police664 uassert.AbortsWithMessage(t, cur,665 "cannot change the guardian while actions are pending",666 func() { SetGuardian(cross(cur), "upgrades", "") },667 )668669 // once nothing is pending, the owner may change the guardian670 Cancel(cross(cur), id)671 SetGuardian(cross(cur), "upgrades", "")672 uassert.Equal(t, "", string(targets["upgrades"].Guardian))673}674675func 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)681682 // re-audit P3: terminal-state writes on expired actions rewrote683 // history; both must refuse and point at the Expire valve684 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}690691func 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)697698 // the next Schedule sweeps expired actions out of state699 Schedule(cross(cur), "upgrades", "op2", MinDelayFloor)700 uassert.Equal(t, 1, len(actions), "expired action must be reaped by the sweep")701}702703func 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)708709 out := Render("")710 uassert.False(t, strings.Contains(out, "[approve]"), "link syntax must be sanitized")711}712713func 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}724725// ---------- round-3 audit regressions (2026-09-02) ----------726727func TestSybilsCannotExhaustSharedCapacity(cur realm, t *testing.T) {728 resetState()729730 // round-3 P1: two attackers filling their OWN per-owner quotas must731 // never block a third owner's Schedule — there is no shared cap left732 // to exhaust733 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 nothing741 }742 }743 }744745 // a legitimate owner is entirely unaffected746 testing.SetRealm(testing.NewUserRealm(owner))747 RegisterTarget(cross(cur), "legit", MinDelayFloor, "")748 Schedule(cross(cur), "legit", "real work", MinDelayFloor) // must not abort749}750751func TestVictimRecoversBehindSybilWall(cur realm, t *testing.T) {752 resetState()753754 // fix Y2/Y4 regression — the exact starvation case the old global755 // budget-windowed sweep could not recover from: a wall of long-delay756 // 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 entries765 }766 }767768 victim := testutils.TestAddress("victim2")769 testing.SetRealm(testing.NewUserRealm(victim))770 RegisterTarget(cross(cur), "vt", MinDelayFloor, guardian)771 var ids []string772 for j := 0; j < MaxPendingPerTarget; j++ {773 ids = append(ids, Schedule(cross(cur), "vt", "op", MinDelayFloor))774 }775 skipSeconds(MinDelayFloor + GracePeriod + 10) // all 20 expire776777 // the victim's own next Schedule sweeps THEIR target despite the wall778 Schedule(cross(cur), "vt", "recovered", MinDelayFloor)779 uassert.Equal(t, 1, pendingCount("vt"), "expired actions swept despite the sybil wall")780781 // and Expire frees a wedged slot directly, from any key782 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}789790func TestPendingCounterConsistency(cur realm, t *testing.T) {791 resetState()792 testing.SetRealm(testing.NewUserRealm(owner))793 registerUpgrades(cur)794795 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"))799800 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"))805806 // expiry: the count drops when the sweep reaps807 skipSeconds(GracePeriod + 10)808 Schedule(cross(cur), "upgrades", "d", MinDelayFloor) // triggers sweep of expired "c"809 uassert.Equal(t, 1, pendingCount("upgrades"))810}811812func 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)817818 out := Render("")819 uassert.False(t, strings.Contains(out, "**ready** _now_"),820 "emphasis markers in Data must be sanitized")821}822823// ---------- render-index bounds (fix Y4) ----------824825func TestRenderIndexSelfTrims(cur realm, t *testing.T) {826 resetState()827 testing.SetRealm(testing.NewUserRealm(owner))828 RegisterTarget(cross(cur), "churn", MinDelayFloor, "")829830 // execute many actions so actionIDs would grow unboundedly without831 // the trim; the index must stay within RenderIndexCap and old832 // executed records must stay queryable via the map reads833 var first string834 for i := 0; i < RenderIndexCap+30; i++ {835 id := Schedule(cross(cur), "churn", "op", MinDelayFloor)836 if i == 0 {837 first = id838 }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}847848// ---------- round-2 audit regressions ----------849850func 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)856857 // round-2 hardening: expired-but-unswept actions protect nothing858 // and must not block a guardian rotation859 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}863864func 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)))869870 // round-2 fix Y-2: flood the render index with executed churn; the871 // live pending action must remain visible for its whole window872 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 }878879 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")882883 // and state stays consistent for an action trimmed out of the884 // 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}888889func 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, "")894895 // round-2 fix Y-1: only targets with live pendings are tracked896 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))900901 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))904905 // registration alone never inflates the list906 RegisterTarget(cross(cur), "three", MinDelayFloor, "")907 uassert.Equal(t, 1, len(pendingTargets))908}909AcceptTargetOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, targetName string)
Cancel(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, actionID string) string
Execute(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, actionID string) string
Expire(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, actionID string) string
GetAction(actionID string) string
GetPending() string
GetTarget(targetName string) string
IsExecuted(actionID string) bool
IsReady(actionID string) bool
RegisterTarget(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, minDelay int64, guardian string)
Render(path string) string
Schedule(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, targetName string, data string, delay int64) string
SetGuardian(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, targetName string, guardian string)
TransferTargetOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, targetName string, newOwner string)
Veto(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, actionID string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.