Transaction

BD83A5558C3494…2345038118F6

Block 336,712 · index 0 · RPC

Summary

Hash
BD83A5558C3494F8273F0B37F764F7197D3DB2C02902F9502A7A2345038118F6
Block
336,712
Size
46556 bytes
Gas used
61,268,850 / 250,000,000
Fee
250000 ugnot
Status
success

Messages

Attached funds
12000000 ugnot

Arguments · 9

  1. #1service_registry
  2. #2gnomod.toml
  3. #3module = "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/service_registry" gno = "0.9"
  4. #4service_registry.gno
  5. #5package service_registry import ( "chain" "chain/runtime/unsafe" "strconv" "strings" "time" "gno.land/p/nt/markdown/sanitize/v0" ) // Service represents a registered on-chain service. type Service struct { Name string Owner address Registrant address // ORIGINAL registrant; immutable through transfers PkgPath string // the realm the service lives at — what integrators resolve Description string ServiceType string // e.g. "token", "dex", "oracle", "dao", "nft", "bridge" Metadata string // freeform key=value pairs or JSON blob } // Reservation holds a deregistered name for its former owner AND its // original registrant, and EXPIRES (re-audit 2026-09-02): an eternal // reservation let an attacker cycle register/deregister to lock the // whole namespace forever, and a hostile transferee could strand a // name against its original registrant permanently. type Reservation struct { Owner address Registrant address Expires time.Time } const ( // MaxServices is a pure state bound, not an anti-squat defense — // MaxServicesPerOwner is what makes monopolization expensive // (pearl audit R1). Kept at 1000 rather than raised further so the // linear scans over `names` (Deregister, ListServices, ListByType) // stay bounded at a size one transaction can comfortably pay for. MaxServices = 1000 // MaxServicesPerOwner caps how many names one address may hold at // once. Enforced at registration and, for a handoff, at the moment // the recipient CONSENTS (see AcceptOwnership). MaxServicesPerOwner = 20 MaxNameLen = 64 MaxTypeLen = 32 MaxPkgPathLen = 128 MaxDescriptionLen = 500 MaxMetadataLen = 2000 pkgPathPrefix = "gno.land/" // ReservationPeriod is how long a deregistered name stays reserved. // Long enough for integrators to notice the deregistration; finite // so tombstones cannot lock the namespace forever. ReservationPeriod = int64(90 * 24 * 3600) // 90 days // MaxRenderServices bounds the gnoweb table. Render is reachable by // any viewer, so its cost lands on third parties rather than on // whoever grew the state (pearl audit Y3). Complete data comes from // the bounded queries named in the truncation notice. MaxRenderServices = 25 // renderDescLen is the per-row description budget, applied to the // RAW text before escaping so an escape sequence is never split. renderDescLen = 60 ) var ( services map[string]*Service names []string // insertion-ordered list for deterministic iteration // retired maps a deregistered name to its time-bounded reservation. retired map[string]*Reservation // ownerServices is the O(1) index backing MaxServicesPerOwner. It is // kept zero-free: an owner that drops to zero is deleted outright so // the map cannot accumulate dead keys. ownerServices map[address]int // pendingOwners holds nominated-but-not-yet-accepted owners // (pearl audit Y4). A name appears here only while a handoff is open. pendingOwners map[string]address ) func init() { services = make(map[string]*Service) names = []string{} retired = make(map[string]*Reservation) ownerServices = make(map[address]int) pendingOwners = make(map[string]address) } // rejectStraySend aborts when coins are attached to a call (pearl audit // Y2): this realm handles no funds, holds no banker and exposes no // withdrawal, so an attached send would strand on the realm address // forever. Aborting reverts the transfer back to the sender. Fails open // for realm-routed calls, whose attached send lands on the intermediary // realm, never here. func rejectStraySend(cur realm) { if cur.Previous().IsUserCall() && len(unsafe.OriginSend()) > 0 { panic("this realm does not accept coins") } } // ---------- quota index ---------- func quotaAdd(owner address) { ownerServices[owner] = ownerServices[owner] + 1 } func quotaDrop(owner address) { n := ownerServices[owner] - 1 if n <= 0 { delete(ownerServices, owner) return } ownerServices[owner] = n } func assertQuota(owner address) { if ownerServices[owner] >= MaxServicesPerOwner { panic("address already holds the maximum of " + strconv.Itoa(MaxServicesPerOwner) + " services") } } // ---------- validation helpers ---------- func isValidName(name string) bool { if name == "" || len(name) > MaxNameLen { return false } for _, c := range name { if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { return false } } return true } func validatePkgPath(pkgPath string) { if pkgPath == "" { panic("pkgpath must not be empty") } if len(pkgPath) > MaxPkgPathLen { panic("pkgpath exceeds " + strconv.Itoa(MaxPkgPathLen) + " chars") } if !strings.HasPrefix(pkgPath, pkgPathPrefix) { panic("pkgpath must start with " + pkgPathPrefix + " and name a package") } // structural, per-element validation (re-audit: the old charset check // accepted "..", "//", trailing slashes, hyphens, and non-r/p roots — // all lookalike-squat or allowlist-bypass material) elems := strings.Split(pkgPath[len(pkgPathPrefix):], "/") if len(elems) < 2 || (elems[0] != "r" && elems[0] != "p") { panic("pkgpath must be gno.land/r/... or gno.land/p/...") } for _, e := range elems[1:] { if e == "" { panic("pkgpath contains an empty element") } if !(e[0] >= 'a' && e[0] <= 'z') { panic("invalid pkgpath element: " + e) } for _, c := range e { if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { panic("invalid pkgpath element: " + e) } } } } func validateFields(description, serviceType, metadata string) { if description == "" { panic("description must not be empty") } if len(description) > MaxDescriptionLen { panic("description exceeds " + strconv.Itoa(MaxDescriptionLen) + " chars") } if serviceType == "" { panic("service type must not be empty") } if len(serviceType) > MaxTypeLen || !isValidName(serviceType) { panic("service type must be lowercase alphanumeric with underscores, max " + strconv.Itoa(MaxTypeLen) + " chars") } if len(metadata) > MaxMetadataLen { panic("metadata exceeds " + strconv.Itoa(MaxMetadataLen) + " chars") } } // truncate shortens s to at most n runes without splitting a multibyte // character. Applied to RAW text, never to escaped output — truncating // after escaping could sever a `\x` pair and leave a dangling backslash. func truncate(s string, n int) string { if n < 4 { panic("truncate: budget too small") } runes := []rune(s) if len(runes) <= n { return s } return string(runes[:n-3]) + "..." } func mustGet(name string) *Service { svc, ok := services[name] if !ok { panic("service not found: " + name) } return svc } // ---------- write operations ---------- // RegisterService adds a new service to the registry. The caller becomes // the owner. Name must be unique, lowercase alphanumeric/underscores. // pkgPath is the realm the service lives at — the field integrators // resolve — and must look like a gno.land package path. A name that was // deregistered stays reserved for its former owner and its original // registrant for ReservationPeriod. // // NOTE: this realm does NOT and cannot verify that pkgPath exists or // that the caller controls it. See the INTEGRATOR CONTRACT on Resolve. func RegisterService(cur realm, name, pkgPath, description, serviceType, metadata string) { rejectStraySend(cur) c := cur.Previous().Address() if !isValidName(name) { panic("name must be 1-" + strconv.Itoa(MaxNameLen) + " chars, lowercase alphanumeric with underscores only") } validatePkgPath(pkgPath) validateFields(description, serviceType, metadata) if _, exists := services[name]; exists { panic("service already registered: " + name) } registrant := c if res, wasRetired := retired[name]; wasRetired { if time.Now().After(res.Expires) { // O(1) lazy reclaim: a lapsed tombstone is dropped the moment // it is consulted, so the map does not pay to keep entries // nobody will ever read again. delete(retired, name) } else { if c != res.Owner && c != res.Registrant { panic("name is reserved for its former owner: " + name) } // round-3 audit: an in-window re-register must PRESERVE the // original registrant — otherwise a hostile transferee could // deregister and instantly re-register, stamping themselves as // registrant and erasing the original project's reclaim right if res.Registrant != "" { registrant = res.Registrant } } } if len(services) >= MaxServices { panic("global service limit reached") } assertQuota(c) services[name] = &Service{ Name: name, Owner: c, Registrant: registrant, PkgPath: pkgPath, Description: description, ServiceType: serviceType, Metadata: metadata, } names = append(names, name) quotaAdd(c) delete(retired, name) chain.Emit("ServiceRegistered", "name", name, "pkgpath", pkgPath, "type", serviceType, "owner", c.String(), "registrant", registrant.String(), ) } // UpdateService modifies a service's pkgpath, description, type, and // metadata. Only the registered owner can update. Name cannot change. // // An update MAY REPOINT the name at a different package path. That is a // deliberate capability (services move, and versions supersede), but it // also means a name an integrator trusts today can point elsewhere // tomorrow. The ServiceUpdated event carries both the old and the new // path specifically so a repoint is observable in the transaction log // rather than something a consumer has to poll for. func UpdateService(cur realm, name, pkgPath, description, serviceType, metadata string) { rejectStraySend(cur) svc := mustGet(name) if cur.Previous().Address() != svc.Owner { panic("only the owner can update this service") } validatePkgPath(pkgPath) validateFields(description, serviceType, metadata) oldPath := svc.PkgPath svc.PkgPath = pkgPath svc.Description = description svc.ServiceType = serviceType svc.Metadata = metadata chain.Emit("ServiceUpdated", "name", name, "oldpkgpath", oldPath, "pkgpath", pkgPath, "type", serviceType, "owner", svc.Owner.String(), ) } // TransferOwnership NOMINATES a new owner for a service entry; the // nominee must call AcceptOwnership to take control (pearl audit Y4). // // The one-step form this replaces was a permanent-brick hazard: // address.IsValid() only checks bech32 form, so a well-formed but // unowned destination passed the check and committed immediately, after // which the entry could never again be updated, transferred or // deregistered — and because it could never be deregistered it could // never enter the reservation window either, so the NAME became a // permanent hole in a shared global namespace. // // Nomination changes nothing: the sitting owner keeps full control // until the nominee consents. Passing "" clears a pending nomination. func TransferOwnership(cur realm, name string, newOwner address) { rejectStraySend(cur) svc := mustGet(name) c := cur.Previous().Address() if c != svc.Owner { panic("only the owner can transfer ownership") } if newOwner == "" { delete(pendingOwners, name) chain.Emit("OwnershipTransferCancelled", "name", name, "owner", c.String()) return } if !newOwner.IsValid() { panic("invalid new owner address: " + string(newOwner)) } if newOwner == svc.Owner { panic("new owner is already the owner") } pendingOwners[name] = newOwner chain.Emit("OwnershipTransferProposed", "name", name, "owner", c.String(), "pending", newOwner.String(), ) } // AcceptOwnership completes a nominated handoff. Only the nominee can // accept, and the nominee's own quota is checked HERE — at consent — // so a nomination can never push an account past MaxServicesPerOwner // without that account agreeing to it. func AcceptOwnership(cur realm, name string) { rejectStraySend(cur) svc := mustGet(name) c := cur.Previous().Address() pending, ok := pendingOwners[name] if !ok { panic("no pending ownership transfer for: " + name) } if c != pending { panic("only the nominated owner can accept") } assertQuota(c) prev := svc.Owner svc.Owner = c quotaDrop(prev) quotaAdd(c) delete(pendingOwners, name) chain.Emit("OwnershipTransferred", "name", name, "from", prev.String(), "to", c.String(), ) } // CancelOwnershipTransfer withdraws a pending nomination. Owner-only. func CancelOwnershipTransfer(cur realm, name string) { rejectStraySend(cur) svc := mustGet(name) c := cur.Previous().Address() if c != svc.Owner { panic("only the owner can cancel a transfer") } if _, ok := pendingOwners[name]; !ok { panic("no pending ownership transfer for: " + name) } delete(pendingOwners, name) chain.Emit("OwnershipTransferCancelled", "name", name, "owner", c.String()) } // Deregister removes a service from the registry. Only the owner can // deregister. The name stays reserved for the former owner and the // original registrant for ReservationPeriod — it cannot be // re-registered by anyone else in that window, so integrators who still // resolve it can never be silently redirected by a squatter. func Deregister(cur realm, name string) { rejectStraySend(cur) svc := mustGet(name) c := cur.Previous().Address() if c != svc.Owner { panic("only the owner can deregister") } retired[name] = &Reservation{ Owner: svc.Owner, Registrant: svc.Registrant, Expires: time.Now().Add(time.Duration(ReservationPeriod) * time.Second), } delete(services, name) quotaDrop(svc.Owner) // a nomination must not outlive the entry it was made against delete(pendingOwners, name) // Remove from ordered list for i, n := range names { if n == name { names = append(names[:i], names[i+1:]...) break } } chain.Emit("ServiceDeregistered", "name", name, "owner", svc.Owner.String(), "registrant", svc.Registrant.String(), ) } // ---------- read-only queries ---------- // Resolve returns the pkgpath a service name points to — the primary // integration query. Panics on unknown names so a consumer can never // silently integrate against a missing entry. // // INTEGRATOR CONTRACT — read this before trusting a resolution: // // 1. A resolution is an ATTESTATION, NOT A PROOF. This realm records // that some address claimed a name for some package path. It does // NOT verify that the path exists, that it is deployed, or that the // registrant controls it. Contrast r/demo/defi/grc20reg, which // proves control by requiring the registered token object to // originate from the calling realm; no equivalent proof exists for // a bare path string, and requiring one would mean only realms — // never their operators — could ever register a name, which is not // this registry's model. // // 2. A NAME IS NOT AN AUTHORIZATION. Never grant a privilege, route a // payment, or admit a caller because Resolve returned its path. // Resolution answers "where does this name point", never "may this // caller act". Derive authority from your own crossing entrypoint's // cur.Previous(), or from an explicit access-control realm. // // 3. THE TARGET CAN CHANGE. The owner may repoint a name at any time // via UpdateService, and ownership itself is transferable. Treat a // resolution as valid only for the transaction that read it; cache // it and you inherit whatever the name points at later. The // ServiceUpdated and OwnershipTransferred events exist so movement // is detectable. func Resolve(name string) string { return mustGet(name).PkgPath } // TryResolve is the non-panicking variant for consumers that need to // degrade gracefully when a name disappears (re-audit: a panicking-only // read path bricks any consumer realm that calls it inline). The // INTEGRATOR CONTRACT documented on Resolve applies here identically. func TryResolve(name string) (string, bool) { svc, ok := services[name] if !ok { return "", false } return svc.PkgPath, true } // GetOwner returns the owner address of a service. func GetOwner(name string) address { return mustGet(name).Owner } // GetPendingOwner returns the nominated-but-not-yet-accepted owner of a // service, or "none" when no handoff is open. func GetPendingOwner(name string) string { mustGet(name) if p, ok := pendingOwners[name]; ok { return p.String() } return "none" } // ServiceCount returns how many services are registered and the global // cap, so a caller can check headroom without pulling the whole list. func ServiceCount() (count, limit int) { return len(services), MaxServices } // OwnerServiceCount returns how many services an address currently holds // and the per-owner cap. func OwnerServiceCount(owner address) (count, limit int) { return ownerServices[owner], MaxServicesPerOwner } // GetService returns a formatted summary of a registered service. Free // text is escaped for a single-line markdown slot. func GetService(name string) string { svc := mustGet(name) var b strings.Builder b.WriteString("Name: " + svc.Name + "\n") b.WriteString("Owner: " + string(svc.Owner) + "\n") b.WriteString("PkgPath: " + svc.PkgPath + "\n") b.WriteString("Type: " + svc.ServiceType + "\n") b.WriteString("Description: " + sanitize.InlineText(svc.Description) + "\n") if svc.Metadata != "" { b.WriteString("Metadata: " + sanitize.InlineText(svc.Metadata) + "\n") } return b.String() } // ListServices returns all registered service names as a comma-separated // string in registration order. // // COST NOTE: this is O(MaxServices) and is deliberately NOT truncated — // an integrator enumerating the registry needs the complete set, and the // caller pays for its own read. Render, whose cost lands on third-party // viewers instead, IS bounded. func ListServices() string { if len(names) == 0 { return "none" } return strings.Join(names, ", ") } // ListByType returns all service names matching a given type. Same cost // note as ListServices. func ListByType(serviceType string) string { var result []string for _, name := range names { svc := services[name] if svc != nil && svc.ServiceType == serviceType { result = append(result, name) } } if len(result) == 0 { return "none" } return strings.Join(result, ", ") } // ---------- render ---------- // Render returns a markdown overview, bounded to MaxRenderServices rows // (pearl audit Y3). Never panics. All free text goes through the // ecosystem sanitizer rather than a bespoke escaper: the hand-rolled // replacement of backticks and pipes it replaces left `[`, `]`, `(`, // `)` and `!` live, so any registrant could inject a working markdown // link or image into a table cell and phish every viewer of this page. func Render(path string) string { total := len(services) if total == 0 { return "# Service Registry\n\nNo services registered.\n" } var b strings.Builder b.WriteString("# Service Registry\n\n") b.WriteString("**Total services:** " + strconv.Itoa(total) + "\n\n") b.WriteString("| Name | Type | PkgPath | Owner | Description |\n") b.WriteString("|------|------|---------|-------|-------------|\n") shown := 0 for _, name := range names { if shown >= MaxRenderServices { break } svc := services[name] if svc == nil { continue } desc := sanitize.TableCell(truncate(svc.Description, renderDescLen)) b.WriteString("| " + svc.Name + " | " + svc.ServiceType + " | `" + svc.PkgPath + "` | `" + string(svc.Owner) + "` | " + desc + " |\n") shown++ } if shown < total { b.WriteString("\n_Showing " + strconv.Itoa(shown) + " of " + strconv.Itoa(total) + " services. Use ListServices, ListByType " + "or GetService for complete data._\n") } b.WriteString("\n") return b.String() }
  6. #6service_registry_test.gno
  7. #7package service_registry import ( "chain" "strconv" "strings" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) var ( owner = testutils.TestAddress("owner") attacker = testutils.TestAddress("attacker") newOwner = testutils.TestAddress("newowner") third = testutils.TestAddress("third") ) const goodPath = "gno.land/r/demo/oracle" // resetState resets all package-level state between tests. func resetState() { services = make(map[string]*Service) names = []string{} retired = make(map[string]*Reservation) ownerServices = make(map[address]int) pendingOwners = make(map[string]address) } func register(cur realm, name string) { testing.SetRealm(testing.NewUserRealm(owner)) RegisterService(cross(cur), name, goodPath, "a price oracle", "oracle", "") } // fillFor registers n services for one address under a name prefix. // // NOTE: testing.SetRealm is FRAME-scoped — the caller realm set here does // not survive the return, so a caller that needs a specific identity // afterwards must set it again itself. func fillFor(cur realm, who address, prefix string, n int) { testing.SetRealm(testing.NewUserRealm(who)) for i := 0; i < n; i++ { RegisterService(cross(cur), prefix+strconv.Itoa(i), goodPath, "d", "oracle", "") } } // handOff runs a COMPLETE two-step ownership transfer: the sitting owner // nominates, the nominee consents. Ownership does not move until the // second call (pearl audit Y4). func handOff(cur realm, from, to address, name string) { testing.SetRealm(testing.NewUserRealm(from)) TransferOwnership(cross(cur), name, to) testing.SetRealm(testing.NewUserRealm(to)) AcceptOwnership(cross(cur), name) } func quotaMsg() string { return "address already holds the maximum of " + strconv.Itoa(MaxServicesPerOwner) + " services" } // ---------- RegisterService ---------- func TestRegister_Basic(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") svc := services["price_oracle"] uassert.Equal(t, string(owner), string(svc.Owner)) uassert.Equal(t, goodPath, svc.PkgPath) uassert.Equal(t, "oracle", svc.ServiceType) } func TestRegister_DuplicateRejected(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "service already registered: price_oracle", func() { RegisterService(cross(cur), "price_oracle", goodPath, "mine now", "oracle", "") }, ) } func TestRegister_NameValidation(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) for _, bad := range []string{"", "Upper", "has space", "semi;colon", strings.Repeat("a", MaxNameLen+1)} { uassert.AbortsContains(t, cur, "name must be", func() { RegisterService(cross(cur), bad, goodPath, "d", "oracle", "") }, ) } } func TestRegister_PkgPathValidation(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) uassert.AbortsWithMessage(t, cur, "pkgpath must not be empty", func() { RegisterService(cross(cur), "svc", "", "d", "oracle", "") }) uassert.AbortsContains(t, cur, "pkgpath must start with", func() { RegisterService(cross(cur), "svc", "example.com/r/x", "d", "oracle", "") }) uassert.AbortsContains(t, cur, "pkgpath must be gno.land/r/", func() { RegisterService(cross(cur), "svc", "gno.land/", "d", "oracle", "") }) uassert.AbortsContains(t, cur, "invalid pkgpath element", func() { RegisterService(cross(cur), "svc", "gno.land/r/x|y", "d", "oracle", "") }) uassert.AbortsContains(t, cur, "pkgpath exceeds", func() { RegisterService(cross(cur), "svc", "gno.land/r/"+strings.Repeat("a", MaxPkgPathLen), "d", "oracle", "") }) } func TestRegister_FieldValidation(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) uassert.AbortsWithMessage(t, cur, "description must not be empty", func() { RegisterService(cross(cur), "svc", goodPath, "", "oracle", "") }) uassert.AbortsContains(t, cur, "description exceeds", func() { RegisterService(cross(cur), "svc", goodPath, strings.Repeat("d", MaxDescriptionLen+1), "oracle", "") }) uassert.AbortsWithMessage(t, cur, "service type must not be empty", func() { RegisterService(cross(cur), "svc", goodPath, "d", "", "") }) uassert.AbortsContains(t, cur, "service type must be", func() { RegisterService(cross(cur), "svc", goodPath, "d", "Bad|Type", "") }) uassert.AbortsContains(t, cur, "metadata exceeds", func() { RegisterService(cross(cur), "svc", goodPath, "d", "oracle", strings.Repeat("m", MaxMetadataLen+1)) }) } // ---------- Deregister + squat protection ---------- func TestDeregister_NameStaysReservedForOwner(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") testing.SetRealm(testing.NewUserRealm(owner)) Deregister(cross(cur), "price_oracle") uassert.PanicsWithMessage(t, cur, "service not found: price_oracle", func() { Resolve("price_oracle") }, ) // the squat the audit found: attacker re-registers the freed name // and inherits every integrator still resolving it — must be blocked testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "name is reserved for its former owner: price_oracle", func() { RegisterService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "trust me", "oracle", "") }, ) // the former owner can re-register it testing.SetRealm(testing.NewUserRealm(owner)) RegisterService(cross(cur), "price_oracle", goodPath, "back again", "oracle", "") uassert.Equal(t, goodPath, Resolve("price_oracle")) } func TestDeregister_OwnerOnly(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "only the owner can deregister", func() { Deregister(cross(cur), "price_oracle") }, ) } // ---------- UpdateService / TransferOwnership ---------- func TestUpdate_OwnerOnly(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "only the owner can update this service", func() { UpdateService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "hijacked", "oracle", "") }, ) testing.SetRealm(testing.NewUserRealm(owner)) UpdateService(cross(cur), "price_oracle", "gno.land/r/demo/oracle_v2", "v2", "oracle", "k=v") uassert.Equal(t, "gno.land/r/demo/oracle_v2", Resolve("price_oracle")) } func TestTransferOwnership(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "only the owner can transfer ownership", func() { TransferOwnership(cross(cur), "price_oracle", attacker) }, ) testing.SetRealm(testing.NewUserRealm(owner)) uassert.AbortsWithMessage(t, cur, "invalid new owner address: junk", func() { TransferOwnership(cross(cur), "price_oracle", address("junk")) }, ) uassert.AbortsWithMessage(t, cur, "new owner is already the owner", func() { TransferOwnership(cross(cur), "price_oracle", owner) }, ) handOff(cur, owner, newOwner, "price_oracle") uassert.Equal(t, string(newOwner), string(GetOwner("price_oracle"))) // the new owner controls the entry now testing.SetRealm(testing.NewUserRealm(newOwner)) UpdateService(cross(cur), "price_oracle", goodPath, "under new management", "oracle", "") } // ---------- queries ---------- func TestResolve_UnknownPanics(cur realm, t *testing.T) { resetState() uassert.PanicsWithMessage(t, cur, "service not found: ghost", func() { Resolve("ghost") }, ) } func TestListServices_And_ByType(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) RegisterService(cross(cur), "alpha", goodPath, "d", "oracle", "") RegisterService(cross(cur), "beta", goodPath, "d", "dex", "") RegisterService(cross(cur), "gamma", goodPath, "d", "oracle", "") uassert.Equal(t, "alpha, beta, gamma", ListServices()) uassert.Equal(t, "alpha, gamma", ListByType("oracle")) uassert.Equal(t, "none", ListByType("bridge")) } func TestGetService_Summary(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") out := GetService("price_oracle") uassert.True(t, strings.Contains(out, "PkgPath: "+goodPath), "summary must include pkgpath") uassert.True(t, strings.Contains(out, string(owner)), "summary must include owner") } // ---------- render ---------- func TestRender_EmptyState(cur realm, t *testing.T) { resetState() uassert.Equal(t, "# Service Registry\n\nNo services registered.\n", Render("")) } func TestRender_SanitizesInjection(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) RegisterService(cross(cur), "sneaky", goodPath, "desc` | fake | row\ninjected", "oracle", "") out := Render("") uassert.False(t, strings.Contains(out, "desc`"), "backticks must be sanitized") uassert.False(t, strings.Contains(out, "| fake |"), "pipes must be sanitized") uassert.False(t, strings.Contains(out, "\ninjected"), "newlines must be sanitized") } func TestRender_ListsEntries(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") out := Render("") uassert.True(t, strings.Contains(out, "price_oracle"), "render must list the service") uassert.True(t, strings.Contains(out, goodPath), "render must show the pkgpath") uassert.False(t, strings.Contains(out, "Showing"), "a short registry must not carry a truncation notice") } // ---------- re-audit regressions (2026-09-02) ---------- // skipSeconds advances chain time (5s per height). func skipSeconds(sec int64) { testing.SkipHeights((sec + 4) / 5) } func TestReservation_ExpiresAndFreesTheName(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") testing.SetRealm(testing.NewUserRealm(owner)) Deregister(cross(cur), "price_oracle") // re-audit P1: eternal tombstones let an attacker lock the namespace // forever; within the window the name stays reserved... testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "name is reserved for its former owner: price_oracle", func() { RegisterService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "d", "oracle", "") }, ) // ...but after ReservationPeriod it frees for anyone. // SkipHeights resets the whole test context, so the caller realm has // to be re-armed after the jump. skipSeconds(ReservationPeriod + 10) testing.SetRealm(testing.NewUserRealm(attacker)) RegisterService(cross(cur), "price_oracle", "gno.land/r/other/oracle", "d", "oracle", "") uassert.Equal(t, "gno.land/r/other/oracle", Resolve("price_oracle")) // a LAPSED reservation confers nothing: the new registrant is stamped // as registrant, and the dead tombstone is reclaimed uassert.Equal(t, string(attacker), string(services["price_oracle"].Registrant), "a lapsed reservation must not preserve the old registrant") _, stillRetired := retired["price_oracle"] uassert.False(t, stillRetired, "a lapsed tombstone must be reclaimed") } func TestReservation_OriginalRegistrantCanReclaim(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") // re-audit P2: hostile transferee deregisters — the ORIGINAL // registrant can still reclaim during the reservation window handOff(cur, owner, attacker, "price_oracle") testing.SetRealm(testing.NewUserRealm(attacker)) Deregister(cross(cur), "price_oracle") testing.SetRealm(testing.NewUserRealm(owner)) RegisterService(cross(cur), "price_oracle", goodPath, "reclaimed", "oracle", "") uassert.Equal(t, string(owner), string(services["price_oracle"].Owner)) } func TestPkgPath_StructuralValidation(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) // re-audit P3: traversal, doubled slashes, trailing slash, hyphens, // and non-r/p roots must all be refused bad := []string{ "gno.land/r/../../etc/passwd", "gno.land//r//demo//oracle", "gno.land/r/demo/oracle/", "gno.land/r/de-mo/oracle", "gno.land/x/demo/oracle", "gno.land/.", } for _, p := range bad { uassert.AbortsContains(t, cur, "pkgpath", func() { RegisterService(cross(cur), "svc", p, "d", "oracle", "") }) } // legitimate r/ and p/ paths still pass RegisterService(cross(cur), "svc_r", "gno.land/r/demo/oracle_v2", "d", "oracle", "") RegisterService(cross(cur), "svc_p", "gno.land/p/nt/avl", "d", "oracle", "") } func TestTryResolve(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") got, ok := TryResolve("price_oracle") uassert.True(t, ok, "known name must resolve") uassert.Equal(t, goodPath, got) _, ok = TryResolve("ghost") uassert.False(t, ok, "unknown name must return ok=false, never panic") } // ---------- round-3 audit regressions (2026-09-02) ---------- func TestReservation_RegistrantSurvivesHostileRecycle(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") // round-3 P2: hostile transferee deregisters and INSTANTLY // re-registers — the original registrant must survive the recycle handOff(cur, owner, attacker, "price_oracle") testing.SetRealm(testing.NewUserRealm(attacker)) Deregister(cross(cur), "price_oracle") RegisterService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "mine", "oracle", "") uassert.Equal(t, string(owner), string(services["price_oracle"].Registrant), "in-window re-register must preserve the original registrant") // so when the attacker abandons again, the original can still reclaim Deregister(cross(cur), "price_oracle") testing.SetRealm(testing.NewUserRealm(owner)) RegisterService(cross(cur), "price_oracle", goodPath, "reclaimed", "oracle", "") uassert.Equal(t, string(owner), string(services["price_oracle"].Owner)) } func TestPkgPath_PinnedBehaviors(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) // round-3 pins: top-level realm path accepted; digit-leading element // and uppercase prefix rejected RegisterService(cross(cur), "toplevel", "gno.land/r/x", "d", "oracle", "") uassert.AbortsContains(t, cur, "invalid pkgpath element", func() { RegisterService(cross(cur), "digits", "gno.land/r/9abc", "d", "oracle", "") }) uassert.AbortsContains(t, cur, "pkgpath must start with", func() { RegisterService(cross(cur), "upper", "GNO.LAND/r/demo/x", "d", "oracle", "") }) } // ---------- pearl audit regressions (2026-09-21) ---------- // R1: one funded key could previously claim the ENTIRE namespace, since // the only bound was the global MaxServices. The per-owner cap makes a // namespace sweep cost MaxServices/MaxServicesPerOwner distinct funded // addresses instead of one. func TestRegister_PerOwnerQuota(cur realm, t *testing.T) { resetState() fillFor(cur, owner, "svc", MaxServicesPerOwner) testing.SetRealm(testing.NewUserRealm(owner)) // fillFor's realm is frame-scoped n, limit := OwnerServiceCount(owner) uassert.Equal(t, MaxServicesPerOwner, n) uassert.Equal(t, MaxServicesPerOwner, limit) uassert.AbortsWithMessage(t, cur, quotaMsg(), func() { RegisterService(cross(cur), "one_more", goodPath, "d", "oracle", "") }) // a DIFFERENT address is completely unaffected — the cap is per-owner, // not a global freeze testing.SetRealm(testing.NewUserRealm(attacker)) RegisterService(cross(cur), "one_more", goodPath, "d", "oracle", "") uassert.Equal(t, goodPath, Resolve("one_more")) // freeing a slot restores headroom testing.SetRealm(testing.NewUserRealm(owner)) Deregister(cross(cur), "svc0") n, _ = OwnerServiceCount(owner) uassert.Equal(t, MaxServicesPerOwner-1, n) RegisterService(cross(cur), "fresh_name", goodPath, "d", "oracle", "") n, _ = OwnerServiceCount(owner) uassert.Equal(t, MaxServicesPerOwner, n) } // The quota index must not accumulate dead keys: an owner that drops to // zero is deleted outright. func TestQuota_IndexStaysZeroFree(cur realm, t *testing.T) { resetState() register(cur, "solo") uassert.Equal(t, 1, len(ownerServices)) testing.SetRealm(testing.NewUserRealm(owner)) Deregister(cross(cur), "solo") n, _ := OwnerServiceCount(owner) uassert.Equal(t, 0, n) uassert.Equal(t, 0, len(ownerServices), "a zeroed owner must leave no key behind") } // The global cap still backstops the per-owner cap. Reaching it through // the public API would take MaxServices/MaxServicesPerOwner distinct // funded addresses, so the state is staged directly (this test is // in-package) and only the guard is exercised. func TestRegister_GlobalCap(cur realm, t *testing.T) { resetState() for i := 0; i < MaxServices; i++ { n := "pad" + strconv.Itoa(i) services[n] = &Service{ Name: n, Owner: attacker, Registrant: attacker, PkgPath: goodPath, Description: "d", ServiceType: "oracle", } names = append(names, n) } count, limit := ServiceCount() uassert.Equal(t, MaxServices, count) uassert.Equal(t, MaxServices, limit) testing.SetRealm(testing.NewUserRealm(owner)) uassert.AbortsWithMessage(t, cur, "global service limit reached", func() { RegisterService(cross(cur), "one_more", goodPath, "d", "oracle", "") }) } // Y2: this realm holds no banker and exposes no withdrawal path, so coins // attached to any entrypoint would strand at the realm address forever. // Every crossing function must abort, which reverts the transfer. func TestStraySendRejected(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") testing.SetRealm(testing.NewUserRealm(owner)) TransferOwnership(cross(cur), "price_oracle", newOwner) msg := "this realm does not accept coins" testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}}) uassert.AbortsWithMessage(t, cur, msg, func() { RegisterService(cross(cur), "paid", goodPath, "d", "oracle", "") }) uassert.AbortsWithMessage(t, cur, msg, func() { UpdateService(cross(cur), "price_oracle", goodPath, "paid", "oracle", "") }) uassert.AbortsWithMessage(t, cur, msg, func() { TransferOwnership(cross(cur), "price_oracle", third) }) uassert.AbortsWithMessage(t, cur, msg, func() { CancelOwnershipTransfer(cross(cur), "price_oracle") }) uassert.AbortsWithMessage(t, cur, msg, func() { Deregister(cross(cur), "price_oracle") }) testing.SetRealm(testing.NewUserRealm(newOwner)) testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}}) uassert.AbortsWithMessage(t, cur, msg, func() { AcceptOwnership(cross(cur), "price_oracle") }) testing.SetOriginSend(nil) // nothing was mutated by any of the rejected calls uassert.Equal(t, string(owner), string(GetOwner("price_oracle"))) uassert.Equal(t, string(newOwner), GetPendingOwner("price_oracle")) uassert.Equal(t, "a price oracle", services["price_oracle"].Description) _, ok := TryResolve("paid") uassert.False(t, ok, "a rejected registration must not stick") } // Y3: Render is reachable by any viewer, so its cost lands on third // parties rather than on whoever grew the state. It must be bounded and // must say so. func TestRender_Bounded(cur realm, t *testing.T) { resetState() fillFor(cur, owner, "a", MaxServicesPerOwner) extra := MaxRenderServices - MaxServicesPerOwner + 1 fillFor(cur, attacker, "b", extra) total := MaxServicesPerOwner + extra count, _ := ServiceCount() uassert.Equal(t, total, count) out := Render("") uassert.True(t, strings.Contains(out, "**Total services:** "+strconv.Itoa(total)), "the header must report the TRUE total, not the rendered count") uassert.Equal(t, MaxRenderServices, strings.Count(out, "`"+goodPath+"`"), "exactly MaxRenderServices rows may be emitted") uassert.True(t, strings.Contains(out, "_Showing "+strconv.Itoa(MaxRenderServices)+" of "+strconv.Itoa(total)+" services."), "a truncated render must disclose the truncation") uassert.True(t, strings.Contains(out, "Use ListServices, ListByType or GetService"), "the notice must name the unbounded queries") } // Y5: the hand-rolled sanitizer replaced only backticks and pipes, leaving // `[`, `]`, `(`, `)` and `!` live — so any registrant could inject a // working markdown link or image into a table cell and phish every viewer. func TestRender_RejectsMarkdownLinkInjection(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) RegisterService(cross(cur), "phish", goodPath, "claim [here](https://evil.example/drain)", "oracle", "") RegisterService(cross(cur), "tracker", goodPath, "![x](https://evil.example/pixel.png)", "oracle", "") out := Render("") uassert.False(t, strings.Contains(out, "]("), "no live markdown link/image syntax may survive into a table cell") uassert.False(t, strings.Contains(out, "!["), "no live markdown image opener may survive into a table cell") // the text itself is still shown — it is neutered, not censored. The // sanitizer escapes `.` too, so the raw host never appears verbatim. uassert.True(t, strings.Contains(out, "evil"), "the payload text must still be visible to the reader") uassert.False(t, strings.Contains(out, "evil.example"), "even the dot is escaped — no autolinkable host survives") // the same class of payload must not survive the single-line summary sum := GetService("phish") uassert.False(t, strings.Contains(sum, "]("), "GetService must escape markdown link syntax too") } // Y4: the one-step transfer this replaces was a permanent-brick hazard. // address.IsValid() only checks bech32 form, so a well-formed but unowned // destination committed immediately, after which the entry could never be // updated, transferred or deregistered — and because it could never be // deregistered it could never enter the reservation window either, so the // NAME became a permanent hole in a shared global namespace. func TestTransferOwnership_TwoStep(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") // nomination alone changes NOTHING testing.SetRealm(testing.NewUserRealm(owner)) TransferOwnership(cross(cur), "price_oracle", newOwner) uassert.Equal(t, string(owner), string(GetOwner("price_oracle"))) uassert.Equal(t, string(newOwner), GetPendingOwner("price_oracle")) // the sitting owner keeps full control while the handoff is open UpdateService(cross(cur), "price_oracle", goodPath, "still mine", "oracle", "") // only the nominee may accept testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "only the nominated owner can accept", func() { AcceptOwnership(cross(cur), "price_oracle") }) testing.SetRealm(testing.NewUserRealm(newOwner)) AcceptOwnership(cross(cur), "price_oracle") uassert.Equal(t, string(newOwner), string(GetOwner("price_oracle"))) uassert.Equal(t, "none", GetPendingOwner("price_oracle")) // the quota moved with the entry n, _ := OwnerServiceCount(owner) uassert.Equal(t, 0, n) n, _ = OwnerServiceCount(newOwner) uassert.Equal(t, 1, n) // the former owner is now an outsider testing.SetRealm(testing.NewUserRealm(owner)) uassert.AbortsWithMessage(t, cur, "only the owner can update this service", func() { UpdateService(cross(cur), "price_oracle", goodPath, "back", "oracle", "") }) } func TestTransferOwnership_Cancel(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") testing.SetRealm(testing.NewUserRealm(owner)) uassert.AbortsWithMessage(t, cur, "no pending ownership transfer for: price_oracle", func() { CancelOwnershipTransfer(cross(cur), "price_oracle") }) TransferOwnership(cross(cur), "price_oracle", newOwner) testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "only the owner can cancel a transfer", func() { CancelOwnershipTransfer(cross(cur), "price_oracle") }) testing.SetRealm(testing.NewUserRealm(owner)) CancelOwnershipTransfer(cross(cur), "price_oracle") uassert.Equal(t, "none", GetPendingOwner("price_oracle")) testing.SetRealm(testing.NewUserRealm(newOwner)) uassert.AbortsWithMessage(t, cur, "no pending ownership transfer for: price_oracle", func() { AcceptOwnership(cross(cur), "price_oracle") }) // re-nominating and then clearing with the empty address is equivalent testing.SetRealm(testing.NewUserRealm(owner)) TransferOwnership(cross(cur), "price_oracle", newOwner) TransferOwnership(cross(cur), "price_oracle", address("")) uassert.Equal(t, "none", GetPendingOwner("price_oracle")) } // The quota is checked at CONSENT, not at nomination, so a nomination can // never push an account past its cap without that account agreeing. func TestTransferOwnership_QuotaCheckedAtConsent(cur realm, t *testing.T) { resetState() fillFor(cur, newOwner, "b", MaxServicesPerOwner) register(cur, "extra") // nomination succeeds even though the nominee is full... testing.SetRealm(testing.NewUserRealm(owner)) TransferOwnership(cross(cur), "extra", newOwner) uassert.Equal(t, string(newOwner), GetPendingOwner("extra")) // ...but consent is refused, and the entry stays with its owner testing.SetRealm(testing.NewUserRealm(newOwner)) uassert.AbortsWithMessage(t, cur, quotaMsg(), func() { AcceptOwnership(cross(cur), "extra") }) uassert.Equal(t, string(owner), string(GetOwner("extra"))) // freeing one slot lets the same handoff complete Deregister(cross(cur), "b0") AcceptOwnership(cross(cur), "extra") uassert.Equal(t, string(newOwner), string(GetOwner("extra"))) n, _ := OwnerServiceCount(newOwner) uassert.Equal(t, MaxServicesPerOwner, n) } // A nomination must not outlive the entry it was made against — otherwise // a stale nominee could seize a name that was deregistered and reclaimed. func TestTransferOwnership_NominationDiesWithEntry(cur realm, t *testing.T) { resetState() register(cur, "price_oracle") testing.SetRealm(testing.NewUserRealm(owner)) TransferOwnership(cross(cur), "price_oracle", attacker) Deregister(cross(cur), "price_oracle") RegisterService(cross(cur), "price_oracle", goodPath, "reclaimed", "oracle", "") uassert.Equal(t, "none", GetPendingOwner("price_oracle")) testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "no pending ownership transfer for: price_oracle", func() { AcceptOwnership(cross(cur), "price_oracle") }) uassert.Equal(t, string(owner), string(GetOwner("price_oracle"))) }
  8. #8/gno.MemPackageType
  9. #9 MPUserAll

Result log

msg:0,success:true,log:,events:[]