service_registry_test.gno
25.59 Kb · 677 lines
1package service_registry
2
3import (
4 "chain"
5 "strconv"
6 "strings"
7 "testing"
8
9 "gno.land/p/nt/testutils/v0"
10 "gno.land/p/nt/uassert/v0"
11)
12
13var (
14 owner = testutils.TestAddress("owner")
15 attacker = testutils.TestAddress("attacker")
16 newOwner = testutils.TestAddress("newowner")
17 third = testutils.TestAddress("third")
18)
19
20const goodPath = "gno.land/r/demo/oracle"
21
22// resetState resets all package-level state between tests.
23func resetState() {
24 services = make(map[string]*Service)
25 names = []string{}
26 retired = make(map[string]*Reservation)
27 ownerServices = make(map[address]int)
28 pendingOwners = make(map[string]address)
29}
30
31func register(cur realm, name string) {
32 testing.SetRealm(testing.NewUserRealm(owner))
33 RegisterService(cross(cur), name, goodPath, "a price oracle", "oracle", "")
34}
35
36// fillFor registers n services for one address under a name prefix.
37//
38// NOTE: testing.SetRealm is FRAME-scoped — the caller realm set here does
39// not survive the return, so a caller that needs a specific identity
40// afterwards must set it again itself.
41func fillFor(cur realm, who address, prefix string, n int) {
42 testing.SetRealm(testing.NewUserRealm(who))
43 for i := 0; i < n; i++ {
44 RegisterService(cross(cur), prefix+strconv.Itoa(i), goodPath, "d", "oracle", "")
45 }
46}
47
48// handOff runs a COMPLETE two-step ownership transfer: the sitting owner
49// nominates, the nominee consents. Ownership does not move until the
50// second call (pearl audit Y4).
51func handOff(cur realm, from, to address, name string) {
52 testing.SetRealm(testing.NewUserRealm(from))
53 TransferOwnership(cross(cur), name, to)
54 testing.SetRealm(testing.NewUserRealm(to))
55 AcceptOwnership(cross(cur), name)
56}
57
58func quotaMsg() string {
59 return "address already holds the maximum of " +
60 strconv.Itoa(MaxServicesPerOwner) + " services"
61}
62
63// ---------- RegisterService ----------
64
65func TestRegister_Basic(cur realm, t *testing.T) {
66 resetState()
67 register(cur, "price_oracle")
68
69 svc := services["price_oracle"]
70 uassert.Equal(t, string(owner), string(svc.Owner))
71 uassert.Equal(t, goodPath, svc.PkgPath)
72 uassert.Equal(t, "oracle", svc.ServiceType)
73}
74
75func TestRegister_DuplicateRejected(cur realm, t *testing.T) {
76 resetState()
77 register(cur, "price_oracle")
78
79 testing.SetRealm(testing.NewUserRealm(attacker))
80 uassert.AbortsWithMessage(t, cur,
81 "service already registered: price_oracle",
82 func() { RegisterService(cross(cur), "price_oracle", goodPath, "mine now", "oracle", "") },
83 )
84}
85
86func TestRegister_NameValidation(cur realm, t *testing.T) {
87 resetState()
88 testing.SetRealm(testing.NewUserRealm(owner))
89
90 for _, bad := range []string{"", "Upper", "has space", "semi;colon", strings.Repeat("a", MaxNameLen+1)} {
91 uassert.AbortsContains(t, cur,
92 "name must be",
93 func() { RegisterService(cross(cur), bad, goodPath, "d", "oracle", "") },
94 )
95 }
96}
97
98func TestRegister_PkgPathValidation(cur realm, t *testing.T) {
99 resetState()
100 testing.SetRealm(testing.NewUserRealm(owner))
101
102 uassert.AbortsWithMessage(t, cur, "pkgpath must not be empty",
103 func() { RegisterService(cross(cur), "svc", "", "d", "oracle", "") })
104 uassert.AbortsContains(t, cur, "pkgpath must start with",
105 func() { RegisterService(cross(cur), "svc", "example.com/r/x", "d", "oracle", "") })
106 uassert.AbortsContains(t, cur, "pkgpath must be gno.land/r/",
107 func() { RegisterService(cross(cur), "svc", "gno.land/", "d", "oracle", "") })
108 uassert.AbortsContains(t, cur, "invalid pkgpath element",
109 func() { RegisterService(cross(cur), "svc", "gno.land/r/x|y", "d", "oracle", "") })
110 uassert.AbortsContains(t, cur, "pkgpath exceeds",
111 func() {
112 RegisterService(cross(cur), "svc", "gno.land/r/"+strings.Repeat("a", MaxPkgPathLen), "d", "oracle", "")
113 })
114}
115
116func TestRegister_FieldValidation(cur realm, t *testing.T) {
117 resetState()
118 testing.SetRealm(testing.NewUserRealm(owner))
119
120 uassert.AbortsWithMessage(t, cur, "description must not be empty",
121 func() { RegisterService(cross(cur), "svc", goodPath, "", "oracle", "") })
122 uassert.AbortsContains(t, cur, "description exceeds",
123 func() {
124 RegisterService(cross(cur), "svc", goodPath, strings.Repeat("d", MaxDescriptionLen+1), "oracle", "")
125 })
126 uassert.AbortsWithMessage(t, cur, "service type must not be empty",
127 func() { RegisterService(cross(cur), "svc", goodPath, "d", "", "") })
128 uassert.AbortsContains(t, cur, "service type must be",
129 func() { RegisterService(cross(cur), "svc", goodPath, "d", "Bad|Type", "") })
130 uassert.AbortsContains(t, cur, "metadata exceeds",
131 func() {
132 RegisterService(cross(cur), "svc", goodPath, "d", "oracle", strings.Repeat("m", MaxMetadataLen+1))
133 })
134}
135
136// ---------- Deregister + squat protection ----------
137
138func TestDeregister_NameStaysReservedForOwner(cur realm, t *testing.T) {
139 resetState()
140 register(cur, "price_oracle")
141
142 testing.SetRealm(testing.NewUserRealm(owner))
143 Deregister(cross(cur), "price_oracle")
144 uassert.PanicsWithMessage(t, cur,
145 "service not found: price_oracle",
146 func() { Resolve("price_oracle") },
147 )
148
149 // the squat the audit found: attacker re-registers the freed name
150 // and inherits every integrator still resolving it — must be blocked
151 testing.SetRealm(testing.NewUserRealm(attacker))
152 uassert.AbortsWithMessage(t, cur,
153 "name is reserved for its former owner: price_oracle",
154 func() {
155 RegisterService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "trust me", "oracle", "")
156 },
157 )
158
159 // the former owner can re-register it
160 testing.SetRealm(testing.NewUserRealm(owner))
161 RegisterService(cross(cur), "price_oracle", goodPath, "back again", "oracle", "")
162 uassert.Equal(t, goodPath, Resolve("price_oracle"))
163}
164
165func TestDeregister_OwnerOnly(cur realm, t *testing.T) {
166 resetState()
167 register(cur, "price_oracle")
168
169 testing.SetRealm(testing.NewUserRealm(attacker))
170 uassert.AbortsWithMessage(t, cur,
171 "only the owner can deregister",
172 func() { Deregister(cross(cur), "price_oracle") },
173 )
174}
175
176// ---------- UpdateService / TransferOwnership ----------
177
178func TestUpdate_OwnerOnly(cur realm, t *testing.T) {
179 resetState()
180 register(cur, "price_oracle")
181
182 testing.SetRealm(testing.NewUserRealm(attacker))
183 uassert.AbortsWithMessage(t, cur,
184 "only the owner can update this service",
185 func() { UpdateService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "hijacked", "oracle", "") },
186 )
187
188 testing.SetRealm(testing.NewUserRealm(owner))
189 UpdateService(cross(cur), "price_oracle", "gno.land/r/demo/oracle_v2", "v2", "oracle", "k=v")
190 uassert.Equal(t, "gno.land/r/demo/oracle_v2", Resolve("price_oracle"))
191}
192
193func TestTransferOwnership(cur realm, t *testing.T) {
194 resetState()
195 register(cur, "price_oracle")
196
197 testing.SetRealm(testing.NewUserRealm(attacker))
198 uassert.AbortsWithMessage(t, cur,
199 "only the owner can transfer ownership",
200 func() { TransferOwnership(cross(cur), "price_oracle", attacker) },
201 )
202
203 testing.SetRealm(testing.NewUserRealm(owner))
204 uassert.AbortsWithMessage(t, cur,
205 "invalid new owner address: junk",
206 func() { TransferOwnership(cross(cur), "price_oracle", address("junk")) },
207 )
208 uassert.AbortsWithMessage(t, cur,
209 "new owner is already the owner",
210 func() { TransferOwnership(cross(cur), "price_oracle", owner) },
211 )
212
213 handOff(cur, owner, newOwner, "price_oracle")
214 uassert.Equal(t, string(newOwner), string(GetOwner("price_oracle")))
215
216 // the new owner controls the entry now
217 testing.SetRealm(testing.NewUserRealm(newOwner))
218 UpdateService(cross(cur), "price_oracle", goodPath, "under new management", "oracle", "")
219}
220
221// ---------- queries ----------
222
223func TestResolve_UnknownPanics(cur realm, t *testing.T) {
224 resetState()
225
226 uassert.PanicsWithMessage(t, cur,
227 "service not found: ghost",
228 func() { Resolve("ghost") },
229 )
230}
231
232func TestListServices_And_ByType(cur realm, t *testing.T) {
233 resetState()
234 testing.SetRealm(testing.NewUserRealm(owner))
235 RegisterService(cross(cur), "alpha", goodPath, "d", "oracle", "")
236 RegisterService(cross(cur), "beta", goodPath, "d", "dex", "")
237 RegisterService(cross(cur), "gamma", goodPath, "d", "oracle", "")
238
239 uassert.Equal(t, "alpha, beta, gamma", ListServices())
240 uassert.Equal(t, "alpha, gamma", ListByType("oracle"))
241 uassert.Equal(t, "none", ListByType("bridge"))
242}
243
244func TestGetService_Summary(cur realm, t *testing.T) {
245 resetState()
246 register(cur, "price_oracle")
247
248 out := GetService("price_oracle")
249 uassert.True(t, strings.Contains(out, "PkgPath: "+goodPath), "summary must include pkgpath")
250 uassert.True(t, strings.Contains(out, string(owner)), "summary must include owner")
251}
252
253// ---------- render ----------
254
255func TestRender_EmptyState(cur realm, t *testing.T) {
256 resetState()
257 uassert.Equal(t, "# Service Registry\n\nNo services registered.\n", Render(""))
258}
259
260func TestRender_SanitizesInjection(cur realm, t *testing.T) {
261 resetState()
262 testing.SetRealm(testing.NewUserRealm(owner))
263 RegisterService(cross(cur), "sneaky", goodPath,
264 "desc` | fake | row\ninjected", "oracle", "")
265
266 out := Render("")
267 uassert.False(t, strings.Contains(out, "desc`"), "backticks must be sanitized")
268 uassert.False(t, strings.Contains(out, "| fake |"), "pipes must be sanitized")
269 uassert.False(t, strings.Contains(out, "\ninjected"), "newlines must be sanitized")
270}
271
272func TestRender_ListsEntries(cur realm, t *testing.T) {
273 resetState()
274 register(cur, "price_oracle")
275
276 out := Render("")
277 uassert.True(t, strings.Contains(out, "price_oracle"), "render must list the service")
278 uassert.True(t, strings.Contains(out, goodPath), "render must show the pkgpath")
279 uassert.False(t, strings.Contains(out, "Showing"),
280 "a short registry must not carry a truncation notice")
281}
282
283// ---------- re-audit regressions (2026-09-02) ----------
284
285// skipSeconds advances chain time (5s per height).
286func skipSeconds(sec int64) {
287 testing.SkipHeights((sec + 4) / 5)
288}
289
290func TestReservation_ExpiresAndFreesTheName(cur realm, t *testing.T) {
291 resetState()
292 register(cur, "price_oracle")
293 testing.SetRealm(testing.NewUserRealm(owner))
294 Deregister(cross(cur), "price_oracle")
295
296 // re-audit P1: eternal tombstones let an attacker lock the namespace
297 // forever; within the window the name stays reserved...
298 testing.SetRealm(testing.NewUserRealm(attacker))
299 uassert.AbortsWithMessage(t, cur,
300 "name is reserved for its former owner: price_oracle",
301 func() { RegisterService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "d", "oracle", "") },
302 )
303
304 // ...but after ReservationPeriod it frees for anyone.
305 // SkipHeights resets the whole test context, so the caller realm has
306 // to be re-armed after the jump.
307 skipSeconds(ReservationPeriod + 10)
308 testing.SetRealm(testing.NewUserRealm(attacker))
309 RegisterService(cross(cur), "price_oracle", "gno.land/r/other/oracle", "d", "oracle", "")
310 uassert.Equal(t, "gno.land/r/other/oracle", Resolve("price_oracle"))
311
312 // a LAPSED reservation confers nothing: the new registrant is stamped
313 // as registrant, and the dead tombstone is reclaimed
314 uassert.Equal(t, string(attacker), string(services["price_oracle"].Registrant),
315 "a lapsed reservation must not preserve the old registrant")
316 _, stillRetired := retired["price_oracle"]
317 uassert.False(t, stillRetired, "a lapsed tombstone must be reclaimed")
318}
319
320func TestReservation_OriginalRegistrantCanReclaim(cur realm, t *testing.T) {
321 resetState()
322 register(cur, "price_oracle")
323
324 // re-audit P2: hostile transferee deregisters — the ORIGINAL
325 // registrant can still reclaim during the reservation window
326 handOff(cur, owner, attacker, "price_oracle")
327 testing.SetRealm(testing.NewUserRealm(attacker))
328 Deregister(cross(cur), "price_oracle")
329
330 testing.SetRealm(testing.NewUserRealm(owner))
331 RegisterService(cross(cur), "price_oracle", goodPath, "reclaimed", "oracle", "")
332 uassert.Equal(t, string(owner), string(services["price_oracle"].Owner))
333}
334
335func TestPkgPath_StructuralValidation(cur realm, t *testing.T) {
336 resetState()
337 testing.SetRealm(testing.NewUserRealm(owner))
338
339 // re-audit P3: traversal, doubled slashes, trailing slash, hyphens,
340 // and non-r/p roots must all be refused
341 bad := []string{
342 "gno.land/r/../../etc/passwd",
343 "gno.land//r//demo//oracle",
344 "gno.land/r/demo/oracle/",
345 "gno.land/r/de-mo/oracle",
346 "gno.land/x/demo/oracle",
347 "gno.land/.",
348 }
349 for _, p := range bad {
350 uassert.AbortsContains(t, cur, "pkgpath",
351 func() { RegisterService(cross(cur), "svc", p, "d", "oracle", "") })
352 }
353 // legitimate r/ and p/ paths still pass
354 RegisterService(cross(cur), "svc_r", "gno.land/r/demo/oracle_v2", "d", "oracle", "")
355 RegisterService(cross(cur), "svc_p", "gno.land/p/nt/avl", "d", "oracle", "")
356}
357
358func TestTryResolve(cur realm, t *testing.T) {
359 resetState()
360 register(cur, "price_oracle")
361
362 got, ok := TryResolve("price_oracle")
363 uassert.True(t, ok, "known name must resolve")
364 uassert.Equal(t, goodPath, got)
365
366 _, ok = TryResolve("ghost")
367 uassert.False(t, ok, "unknown name must return ok=false, never panic")
368}
369
370// ---------- round-3 audit regressions (2026-09-02) ----------
371
372func TestReservation_RegistrantSurvivesHostileRecycle(cur realm, t *testing.T) {
373 resetState()
374 register(cur, "price_oracle")
375
376 // round-3 P2: hostile transferee deregisters and INSTANTLY
377 // re-registers — the original registrant must survive the recycle
378 handOff(cur, owner, attacker, "price_oracle")
379 testing.SetRealm(testing.NewUserRealm(attacker))
380 Deregister(cross(cur), "price_oracle")
381 RegisterService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "mine", "oracle", "")
382 uassert.Equal(t, string(owner), string(services["price_oracle"].Registrant),
383 "in-window re-register must preserve the original registrant")
384
385 // so when the attacker abandons again, the original can still reclaim
386 Deregister(cross(cur), "price_oracle")
387 testing.SetRealm(testing.NewUserRealm(owner))
388 RegisterService(cross(cur), "price_oracle", goodPath, "reclaimed", "oracle", "")
389 uassert.Equal(t, string(owner), string(services["price_oracle"].Owner))
390}
391
392func TestPkgPath_PinnedBehaviors(cur realm, t *testing.T) {
393 resetState()
394 testing.SetRealm(testing.NewUserRealm(owner))
395
396 // round-3 pins: top-level realm path accepted; digit-leading element
397 // and uppercase prefix rejected
398 RegisterService(cross(cur), "toplevel", "gno.land/r/x", "d", "oracle", "")
399 uassert.AbortsContains(t, cur, "invalid pkgpath element",
400 func() { RegisterService(cross(cur), "digits", "gno.land/r/9abc", "d", "oracle", "") })
401 uassert.AbortsContains(t, cur, "pkgpath must start with",
402 func() { RegisterService(cross(cur), "upper", "GNO.LAND/r/demo/x", "d", "oracle", "") })
403}
404
405// ---------- pearl audit regressions (2026-09-21) ----------
406
407// R1: one funded key could previously claim the ENTIRE namespace, since
408// the only bound was the global MaxServices. The per-owner cap makes a
409// namespace sweep cost MaxServices/MaxServicesPerOwner distinct funded
410// addresses instead of one.
411func TestRegister_PerOwnerQuota(cur realm, t *testing.T) {
412 resetState()
413 fillFor(cur, owner, "svc", MaxServicesPerOwner)
414 testing.SetRealm(testing.NewUserRealm(owner)) // fillFor's realm is frame-scoped
415
416 n, limit := OwnerServiceCount(owner)
417 uassert.Equal(t, MaxServicesPerOwner, n)
418 uassert.Equal(t, MaxServicesPerOwner, limit)
419
420 uassert.AbortsWithMessage(t, cur, quotaMsg(),
421 func() { RegisterService(cross(cur), "one_more", goodPath, "d", "oracle", "") })
422
423 // a DIFFERENT address is completely unaffected — the cap is per-owner,
424 // not a global freeze
425 testing.SetRealm(testing.NewUserRealm(attacker))
426 RegisterService(cross(cur), "one_more", goodPath, "d", "oracle", "")
427 uassert.Equal(t, goodPath, Resolve("one_more"))
428
429 // freeing a slot restores headroom
430 testing.SetRealm(testing.NewUserRealm(owner))
431 Deregister(cross(cur), "svc0")
432 n, _ = OwnerServiceCount(owner)
433 uassert.Equal(t, MaxServicesPerOwner-1, n)
434 RegisterService(cross(cur), "fresh_name", goodPath, "d", "oracle", "")
435 n, _ = OwnerServiceCount(owner)
436 uassert.Equal(t, MaxServicesPerOwner, n)
437}
438
439// The quota index must not accumulate dead keys: an owner that drops to
440// zero is deleted outright.
441func TestQuota_IndexStaysZeroFree(cur realm, t *testing.T) {
442 resetState()
443 register(cur, "solo")
444 uassert.Equal(t, 1, len(ownerServices))
445
446 testing.SetRealm(testing.NewUserRealm(owner))
447 Deregister(cross(cur), "solo")
448 n, _ := OwnerServiceCount(owner)
449 uassert.Equal(t, 0, n)
450 uassert.Equal(t, 0, len(ownerServices), "a zeroed owner must leave no key behind")
451}
452
453// The global cap still backstops the per-owner cap. Reaching it through
454// the public API would take MaxServices/MaxServicesPerOwner distinct
455// funded addresses, so the state is staged directly (this test is
456// in-package) and only the guard is exercised.
457func TestRegister_GlobalCap(cur realm, t *testing.T) {
458 resetState()
459 for i := 0; i < MaxServices; i++ {
460 n := "pad" + strconv.Itoa(i)
461 services[n] = &Service{
462 Name: n, Owner: attacker, Registrant: attacker,
463 PkgPath: goodPath, Description: "d", ServiceType: "oracle",
464 }
465 names = append(names, n)
466 }
467 count, limit := ServiceCount()
468 uassert.Equal(t, MaxServices, count)
469 uassert.Equal(t, MaxServices, limit)
470
471 testing.SetRealm(testing.NewUserRealm(owner))
472 uassert.AbortsWithMessage(t, cur, "global service limit reached",
473 func() { RegisterService(cross(cur), "one_more", goodPath, "d", "oracle", "") })
474}
475
476// Y2: this realm holds no banker and exposes no withdrawal path, so coins
477// attached to any entrypoint would strand at the realm address forever.
478// Every crossing function must abort, which reverts the transfer.
479func TestStraySendRejected(cur realm, t *testing.T) {
480 resetState()
481 register(cur, "price_oracle")
482 testing.SetRealm(testing.NewUserRealm(owner))
483 TransferOwnership(cross(cur), "price_oracle", newOwner)
484
485 msg := "this realm does not accept coins"
486 testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}})
487 uassert.AbortsWithMessage(t, cur, msg,
488 func() { RegisterService(cross(cur), "paid", goodPath, "d", "oracle", "") })
489 uassert.AbortsWithMessage(t, cur, msg,
490 func() { UpdateService(cross(cur), "price_oracle", goodPath, "paid", "oracle", "") })
491 uassert.AbortsWithMessage(t, cur, msg,
492 func() { TransferOwnership(cross(cur), "price_oracle", third) })
493 uassert.AbortsWithMessage(t, cur, msg,
494 func() { CancelOwnershipTransfer(cross(cur), "price_oracle") })
495 uassert.AbortsWithMessage(t, cur, msg,
496 func() { Deregister(cross(cur), "price_oracle") })
497
498 testing.SetRealm(testing.NewUserRealm(newOwner))
499 testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}})
500 uassert.AbortsWithMessage(t, cur, msg,
501 func() { AcceptOwnership(cross(cur), "price_oracle") })
502 testing.SetOriginSend(nil)
503
504 // nothing was mutated by any of the rejected calls
505 uassert.Equal(t, string(owner), string(GetOwner("price_oracle")))
506 uassert.Equal(t, string(newOwner), GetPendingOwner("price_oracle"))
507 uassert.Equal(t, "a price oracle", services["price_oracle"].Description)
508 _, ok := TryResolve("paid")
509 uassert.False(t, ok, "a rejected registration must not stick")
510}
511
512// Y3: Render is reachable by any viewer, so its cost lands on third
513// parties rather than on whoever grew the state. It must be bounded and
514// must say so.
515func TestRender_Bounded(cur realm, t *testing.T) {
516 resetState()
517 fillFor(cur, owner, "a", MaxServicesPerOwner)
518 extra := MaxRenderServices - MaxServicesPerOwner + 1
519 fillFor(cur, attacker, "b", extra)
520
521 total := MaxServicesPerOwner + extra
522 count, _ := ServiceCount()
523 uassert.Equal(t, total, count)
524
525 out := Render("")
526 uassert.True(t, strings.Contains(out, "**Total services:** "+strconv.Itoa(total)),
527 "the header must report the TRUE total, not the rendered count")
528 uassert.Equal(t, MaxRenderServices, strings.Count(out, "`"+goodPath+"`"),
529 "exactly MaxRenderServices rows may be emitted")
530 uassert.True(t, strings.Contains(out,
531 "_Showing "+strconv.Itoa(MaxRenderServices)+" of "+strconv.Itoa(total)+" services."),
532 "a truncated render must disclose the truncation")
533 uassert.True(t, strings.Contains(out, "Use ListServices, ListByType or GetService"),
534 "the notice must name the unbounded queries")
535}
536
537// Y5: the hand-rolled sanitizer replaced only backticks and pipes, leaving
538// `[`, `]`, `(`, `)` and `!` live — so any registrant could inject a
539// working markdown link or image into a table cell and phish every viewer.
540func TestRender_RejectsMarkdownLinkInjection(cur realm, t *testing.T) {
541 resetState()
542 testing.SetRealm(testing.NewUserRealm(owner))
543 RegisterService(cross(cur), "phish", goodPath,
544 "claim [here](https://evil.example/drain)", "oracle", "")
545 RegisterService(cross(cur), "tracker", goodPath,
546 "", "oracle", "")
547
548 out := Render("")
549 uassert.False(t, strings.Contains(out, "]("),
550 "no live markdown link/image syntax may survive into a table cell")
551 uassert.False(t, strings.Contains(out, ",
563 "GetService must escape markdown link syntax too")
564}
565
566// Y4: the one-step transfer this replaces was a permanent-brick hazard.
567// address.IsValid() only checks bech32 form, so a well-formed but unowned
568// destination committed immediately, after which the entry could never be
569// updated, transferred or deregistered — and because it could never be
570// deregistered it could never enter the reservation window either, so the
571// NAME became a permanent hole in a shared global namespace.
572func TestTransferOwnership_TwoStep(cur realm, t *testing.T) {
573 resetState()
574 register(cur, "price_oracle")
575
576 // nomination alone changes NOTHING
577 testing.SetRealm(testing.NewUserRealm(owner))
578 TransferOwnership(cross(cur), "price_oracle", newOwner)
579 uassert.Equal(t, string(owner), string(GetOwner("price_oracle")))
580 uassert.Equal(t, string(newOwner), GetPendingOwner("price_oracle"))
581
582 // the sitting owner keeps full control while the handoff is open
583 UpdateService(cross(cur), "price_oracle", goodPath, "still mine", "oracle", "")
584
585 // only the nominee may accept
586 testing.SetRealm(testing.NewUserRealm(attacker))
587 uassert.AbortsWithMessage(t, cur, "only the nominated owner can accept",
588 func() { AcceptOwnership(cross(cur), "price_oracle") })
589
590 testing.SetRealm(testing.NewUserRealm(newOwner))
591 AcceptOwnership(cross(cur), "price_oracle")
592 uassert.Equal(t, string(newOwner), string(GetOwner("price_oracle")))
593 uassert.Equal(t, "none", GetPendingOwner("price_oracle"))
594
595 // the quota moved with the entry
596 n, _ := OwnerServiceCount(owner)
597 uassert.Equal(t, 0, n)
598 n, _ = OwnerServiceCount(newOwner)
599 uassert.Equal(t, 1, n)
600
601 // the former owner is now an outsider
602 testing.SetRealm(testing.NewUserRealm(owner))
603 uassert.AbortsWithMessage(t, cur, "only the owner can update this service",
604 func() { UpdateService(cross(cur), "price_oracle", goodPath, "back", "oracle", "") })
605}
606
607func TestTransferOwnership_Cancel(cur realm, t *testing.T) {
608 resetState()
609 register(cur, "price_oracle")
610
611 testing.SetRealm(testing.NewUserRealm(owner))
612 uassert.AbortsWithMessage(t, cur, "no pending ownership transfer for: price_oracle",
613 func() { CancelOwnershipTransfer(cross(cur), "price_oracle") })
614
615 TransferOwnership(cross(cur), "price_oracle", newOwner)
616 testing.SetRealm(testing.NewUserRealm(attacker))
617 uassert.AbortsWithMessage(t, cur, "only the owner can cancel a transfer",
618 func() { CancelOwnershipTransfer(cross(cur), "price_oracle") })
619
620 testing.SetRealm(testing.NewUserRealm(owner))
621 CancelOwnershipTransfer(cross(cur), "price_oracle")
622 uassert.Equal(t, "none", GetPendingOwner("price_oracle"))
623
624 testing.SetRealm(testing.NewUserRealm(newOwner))
625 uassert.AbortsWithMessage(t, cur, "no pending ownership transfer for: price_oracle",
626 func() { AcceptOwnership(cross(cur), "price_oracle") })
627
628 // re-nominating and then clearing with the empty address is equivalent
629 testing.SetRealm(testing.NewUserRealm(owner))
630 TransferOwnership(cross(cur), "price_oracle", newOwner)
631 TransferOwnership(cross(cur), "price_oracle", address(""))
632 uassert.Equal(t, "none", GetPendingOwner("price_oracle"))
633}
634
635// The quota is checked at CONSENT, not at nomination, so a nomination can
636// never push an account past its cap without that account agreeing.
637func TestTransferOwnership_QuotaCheckedAtConsent(cur realm, t *testing.T) {
638 resetState()
639 fillFor(cur, newOwner, "b", MaxServicesPerOwner)
640 register(cur, "extra")
641
642 // nomination succeeds even though the nominee is full...
643 testing.SetRealm(testing.NewUserRealm(owner))
644 TransferOwnership(cross(cur), "extra", newOwner)
645 uassert.Equal(t, string(newOwner), GetPendingOwner("extra"))
646
647 // ...but consent is refused, and the entry stays with its owner
648 testing.SetRealm(testing.NewUserRealm(newOwner))
649 uassert.AbortsWithMessage(t, cur, quotaMsg(),
650 func() { AcceptOwnership(cross(cur), "extra") })
651 uassert.Equal(t, string(owner), string(GetOwner("extra")))
652
653 // freeing one slot lets the same handoff complete
654 Deregister(cross(cur), "b0")
655 AcceptOwnership(cross(cur), "extra")
656 uassert.Equal(t, string(newOwner), string(GetOwner("extra")))
657 n, _ := OwnerServiceCount(newOwner)
658 uassert.Equal(t, MaxServicesPerOwner, n)
659}
660
661// A nomination must not outlive the entry it was made against — otherwise
662// a stale nominee could seize a name that was deregistered and reclaimed.
663func TestTransferOwnership_NominationDiesWithEntry(cur realm, t *testing.T) {
664 resetState()
665 register(cur, "price_oracle")
666
667 testing.SetRealm(testing.NewUserRealm(owner))
668 TransferOwnership(cross(cur), "price_oracle", attacker)
669 Deregister(cross(cur), "price_oracle")
670 RegisterService(cross(cur), "price_oracle", goodPath, "reclaimed", "oracle", "")
671 uassert.Equal(t, "none", GetPendingOwner("price_oracle"))
672
673 testing.SetRealm(testing.NewUserRealm(attacker))
674 uassert.AbortsWithMessage(t, cur, "no pending ownership transfer for: price_oracle",
675 func() { AcceptOwnership(cross(cur), "price_oracle") })
676 uassert.Equal(t, string(owner), string(GetOwner("price_oracle")))
677}