registry.gno
5.47 Kb · 146 lines
1// Package registry is an open, on-chain map from a deployed gno package path
2// back to the source that produced it: repository, commit, directory.
3//
4// It is the realm half of gno.land/p/moul/gnopm/v0, which holds the domain
5// model and the whole of the reasoning; this file is wiring. Every exported
6// mutation is a crossing function that resolves the caller, forwards to the
7// library, aborts on error (the only way to revert state in gno) and emits an
8// event for indexers.
9//
10// # It cannot promise, and does not pretend to
11//
12// Nothing here is verified and nothing here can be. A realm cannot clone a
13// repository, so it cannot check that the bytes at the claimed commit are the
14// bytes deployed at the claimed path. What it stores is testimony under a
15// signature: this address says this package came from that source.
16//
17// That is still worth storing, for two reasons.
18//
19// First, it is testimony in the exact shape a verifier needs. Path, repository,
20// commit and directory are precisely the four inputs to "hash the addpkg
21// payload of that directory and compare it with what the chain hands back",
22// which gnopm already does against a local tree (`gnopm verify -deployed`). The
23// registry does not answer the question; it makes the question answerable by
24// anything that can clone.
25//
26// Second, the chain knows who signed. A claim from the address that owns the
27// package path's namespace comes from the party that controls the path, and
28// OwnedBy reports that. It is computed on every read, never stored, because a
29// name can be transferred and a stored answer would rot into exactly the kind
30// of stale claim this realm exists to distinguish from a live one.
31//
32// # Open, and tagged
33//
34// Anyone may claim any path, including one they had nothing to do with. That is
35// deliberate: gating registration on namespace ownership would leave the map
36// empty on day one, when almost nothing has been registered by its own
37// deployer, and an empty registry teaches nobody anything. A stranger's claim
38// is not suppressed, it is labelled and ranked below the owner's, and Render
39// never presents an unverified claim as a fact.
40//
41// A claimant may always withdraw their own claim, and may never touch anyone
42// else's.
43package registry
44
45import (
46 "chain"
47 "chain/runtime"
48
49 pm "gno.land/p/moul/gnopm/v0"
50 "gno.land/r/sys/users"
51)
52
53var reg = pm.New()
54
55// caller is the address that crossed into this realm. Deliberately not
56// restricted to end users: a realm may hold a namespace, and a DAO registering
57// the source of the packages it governs is the same operation.
58func caller(cur realm) address {
59 if !cur.IsCurrent() {
60 panic("gnopm/registry: spoofed realm")
61 }
62 return cur.Previous().Address()
63}
64
65func must(err error) {
66 if err != nil {
67 panic(err)
68 }
69}
70
71// resolveName is the r/sys/users half of the ownership rule, passed to the
72// library so the library stays pure.
73//
74// isCurrent is deliberately ignored, matching r/moul/forge: a name that
75// resolves through an alias still resolves to the same holder, so a namespace
76// survives its owner renaming. IsDeleted folds the nil check in, which is what
77// its own doc comment asks call sites to rely on.
78func resolveName(name string) (address, bool) {
79 data, _ := users.ResolveName(name)
80 if data.IsDeleted() {
81 return "", false
82 }
83 return data.Addr(), true
84}
85
86// Register records that pkgPath was built from dir of repo at commit.
87//
88// Calling it again for the same path replaces the caller's own claim and
89// nobody else's, so moving a claim to a new commit after a redeploy is one
90// call with no read first.
91//
92// ref is optional and is never the thing verified: a ref moves, a commit does
93// not. It is recorded so a reader can tell a claim pinned to a released tag
94// from one pinned to a commit on nobody's branch, and so a verifier can report
95// a commit that has since been orphaned by a force-push.
96//
97// dir is empty when the package sits at the repository root.
98func Register(cur realm, pkgPath, repo, commit, dir, ref string) {
99 a := caller(cur)
100 _, err := reg.Register(a, runtime.ChainHeight(), pkgPath, repo, commit, dir, ref)
101 must(err)
102 chain.Emit("SourceClaimed",
103 "pkgpath", pkgPath,
104 "claimant", a.String(),
105 "repo", repo,
106 "commit", commit,
107 )
108}
109
110// Withdraw removes the caller's own claim about pkgPath.
111func Withdraw(cur realm, pkgPath string) {
112 a := caller(cur)
113 must(reg.Withdraw(a, pkgPath))
114 chain.Emit("SourceWithdrawn", "pkgpath", pkgPath, "claimant", a.String())
115}
116
117// PackageCount is how many package paths carry at least one claim.
118func PackageCount() int { return reg.Size() }
119
120// ClaimCount is the total number of claims across every path.
121func ClaimCount() int { return reg.Claims() }
122
123// HasClaims reports whether anything has been said about pkgPath. Cheap enough
124// for another realm or an indexer to ask per package.
125func HasClaims(pkgPath string) bool { return reg.Package(pkgPath) != nil }
126
127// OwnerClaim returns the repository, commit and directory claimed by the party
128// that owns pkgPath's namespace, and whether such a claim exists.
129//
130// This is the only read that filters, and it filters on the one signal the
131// chain can actually prove. Everything else a caller wants is in Render or in
132// the events.
133func OwnerClaim(pkgPath string) (repo, commit, dir string, ok bool) {
134 p := reg.Package(pkgPath)
135 if p == nil {
136 return "", "", "", false
137 }
138 p.IterateClaims(func(c *pm.Claim) bool {
139 if c.OwnedBy(resolveName) {
140 repo, commit, dir, ok = c.Repo, c.Commit, c.Dir, true
141 return true // stop
142 }
143 return false
144 })
145 return repo, commit, dir, ok
146}