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

registry.gno

9.21 Kb · 259 lines
  1// Package gnopm is the domain model of a source registry for gno packages: a
  2// map from a deployed package path back to the repository, commit and directory
  3// that produced it.
  4//
  5// It exists because gno deliberately broke the link Go gets for free. A Go
  6// import path IS a repository URL, so pkg.go.dev needs no registry; a gno
  7// import path is a chain address, so nothing anywhere says which source
  8// produced the bytes running at gno.land/p/nt/tinyavl/v0.
  9//
 10// # What this can and cannot promise
 11//
 12// It cannot promise anything, and the design is built around saying so rather
 13// than around hiding it. Two claims look alike and are not the same:
 14//
 15//  1. "this package came from that repository". UNPROVABLE, here or anywhere.
 16//     Anyone may deploy any bytes and anyone may claim any repository. No
 17//     registry design fixes this, and one that implies otherwise is worse than
 18//     none.
 19//  2. "the bytes live at path P equal the addpkg payload of directory D in
 20//     repository R at commit C". Mechanically provable, by hashing both sides.
 21//     It is NOT provable here: a realm cannot clone a repository. It is
 22//     provable off chain by anything that can, and gnopm already does exactly
 23//     this against a local tree (`gnopm verify -deployed`).
 24//
 25// So this package stores (1), labelled as the claim it is, in the form (2)
 26// needs to be checkable. A Claim is testimony under a signature. It becomes
 27// evidence only once something outside the chain has gone and looked.
 28//
 29// # What a signature does buy
 30//
 31// The chain knows who signed a claim, and that is not nothing. A claim whose
 32// claimant owns the package path's namespace comes from the party that controls
 33// the path, which is the closest thing to a promise available without a
 34// chain-level feature. OwnedBy reports that relationship; it is deliberately
 35// computed on demand from a caller-supplied resolver and never stored, because
 36// a name can be transferred and a stored answer would be a claim of its own.
 37//
 38// Anyone may still claim any path. That is the "open but tagged" choice, and it
 39// is made on purpose: gating registration on namespace ownership would mean
 40// that on day one, when almost nothing is registered by its own deployer, the
 41// registry is empty and teaches nobody anything. The defence against a
 42// misleading claim is that it renders as a stranger's claim, ranked below, and
 43// never as a fact.
 44package gnopm
 45
 46import (
 47	"strings"
 48
 49	"gno.land/p/nt/avl/v0"
 50)
 51
 52// Registry maps a package path to the claims made about its source.
 53//
 54// One path may carry several claims, at most one per claimant. That is not a
 55// flaw to be resolved: a fork is a legitimate second answer, and so is a third
 56// party filling in the map for a package whose author never will.
 57type Registry struct {
 58	packages *avl.Tree // package path -> *Package
 59	claims   int
 60}
 61
 62// New returns an empty registry.
 63func New() *Registry {
 64	return &Registry{packages: avl.NewTree()}
 65}
 66
 67// Package is every claim made about one package path.
 68type Package struct {
 69	PkgPath string
 70	claims  *avl.Tree // claimant address string -> *Claim
 71}
 72
 73// Claim is one address's statement about where a package came from.
 74//
 75// Every field except Claimant, Height and UpdatedAt is attacker-controlled.
 76// They are charset-validated at write time rather than escaped at read time, so
 77// that a consumer which forgets to escape is still safe: the fields cannot hold
 78// a byte that means anything to markdown, to a terminal or to a URL parser.
 79type Claim struct {
 80	PkgPath  string
 81	Repo     string // https:// URL of the repository
 82	Commit   string // 40 or 64 lowercase hex
 83	Dir      string // subdirectory holding the package, "" for the repo root
 84	Ref      string // fully-qualified ref the commit was on, optional
 85	Claimant address
 86	Height   int64 // block height of the first claim by this claimant
 87	// UpdatedAt is the height of the most recent write. Equal to Height until
 88	// the claimant re-registers, which is how a reader tells a claim that has
 89	// been kept current from one made once and abandoned.
 90	UpdatedAt int64
 91}
 92
 93// Register records or replaces claimant's claim about pkgPath.
 94//
 95// Re-registering the same path overwrites that claimant's own previous claim
 96// and nobody else's, so the ordinary "I deployed a new commit" flow is one call
 97// with no read first. Height is preserved across an update: it dates the claim,
 98// not the edit.
 99func (r *Registry) Register(claimant address, height int64, pkgPath, repo, commit, dir, ref string) (*Claim, error) {
100	if !ValidPkgPath(pkgPath) {
101		return nil, ErrInvalidPkgPath
102	}
103	if !ValidRepo(repo) {
104		return nil, ErrInvalidRepo
105	}
106	if !ValidCommit(commit) {
107		return nil, ErrInvalidCommit
108	}
109	if !ValidDir(dir) {
110		return nil, ErrInvalidDir
111	}
112	if !ValidRef(ref) {
113		return nil, ErrInvalidRef
114	}
115
116	p := r.Package(pkgPath)
117	if p == nil {
118		p = &Package{PkgPath: pkgPath, claims: avl.NewTree()}
119		r.packages.Set(pkgPath, p)
120	}
121
122	key := claimant.String()
123	c := &Claim{
124		PkgPath:   pkgPath,
125		Repo:      repo,
126		Commit:    commit,
127		Dir:       dir,
128		Ref:       ref,
129		Claimant:  claimant,
130		Height:    height,
131		UpdatedAt: height,
132	}
133	if prev := p.Claim(claimant); prev != nil {
134		c.Height = prev.Height
135	} else {
136		if p.claims.Size() >= MaxClaimants {
137			return nil, ErrTooManyClaimants
138		}
139		r.claims++
140	}
141	p.claims.Set(key, c)
142	return c, nil
143}
144
145// Withdraw removes claimant's own claim about pkgPath. A claimant can always
146// take back what they said; nobody can remove anyone else's.
147func (r *Registry) Withdraw(claimant address, pkgPath string) error {
148	p := r.Package(pkgPath)
149	if p == nil {
150		return ErrClaimNotFound
151	}
152	if _, removed := p.claims.Remove(claimant.String()); !removed {
153		return ErrClaimNotFound
154	}
155	r.claims--
156	if p.claims.Size() == 0 {
157		r.packages.Remove(pkgPath)
158	}
159	return nil
160}
161
162// Package returns every claim about a path, or nil.
163func (r *Registry) Package(pkgPath string) *Package {
164	v := r.packages.Get(pkgPath)
165	if v == nil {
166		return nil
167	}
168	return v.(*Package)
169}
170
171// Size is the number of package paths carrying at least one claim.
172func (r *Registry) Size() int { return r.packages.Size() }
173
174// Claims is the total number of claims across every path.
175func (r *Registry) Claims() int { return r.claims }
176
177// IteratePackages walks paths in lexical order, which groups a namespace's
178// packages together without needing a second index. The callback returns true
179// to STOP, following avl's own convention rather than inverting it here.
180func (r *Registry) IteratePackages(offset, count int, cb func(*Package) bool) {
181	r.packages.IterateByOffset(offset, count, func(_ string, v any) bool {
182		return cb(v.(*Package))
183	})
184}
185
186// IterateNamespace walks every claimed path under "<domain>/<kind>/<ns>/".
187func (r *Registry) IterateNamespace(domain, kind, ns string, cb func(*Package) bool) {
188	prefix := domain + "/" + kind + "/" + ns + "/"
189	r.packages.Iterate(prefix, "", func(k string, v any) bool {
190		if !strings.HasPrefix(k, prefix) {
191			return true // past the prefix: stop
192		}
193		return cb(v.(*Package))
194	})
195}
196
197// Claim returns claimant's claim about this package, or nil.
198func (p *Package) Claim(claimant address) *Claim {
199	v := p.claims.Get(claimant.String())
200	if v == nil {
201		return nil
202	}
203	return v.(*Claim)
204}
205
206// Count is how many addresses have claimed this package.
207func (p *Package) Count() int { return p.claims.Size() }
208
209// IterateClaims walks the claims in claimant-address order. Order is not
210// significance: a caller that wants the owner's claim first must ask OwnedBy,
211// because the registry has no opinion about which claim is true.
212func (p *Package) IterateClaims(cb func(*Claim) bool) {
213	p.claims.Iterate("", "", func(_ string, v any) bool {
214		return cb(v.(*Claim))
215	})
216}
217
218// OwnedBy reports whether c comes from the party that controls the package
219// path's namespace: either the address whose own namespace it is, or whoever
220// resolve says holds the registered name.
221//
222// resolve maps a namespace name to the address holding it and whether that name
223// is held at all. It is a parameter rather than an import because this package
224// stays pure: the realm supplies r/sys/users, and a test supplies a table.
225//
226// This is the one label in the whole design that is derived rather than
227// declared, so it is computed here and never stored. A name can be transferred;
228// a stored answer would age into a lie of exactly the kind this package exists
229// to avoid.
230func (c *Claim) OwnedBy(resolve func(name string) (address, bool)) bool {
231	_, _, ns, _, ok := SplitPkgPath(c.PkgPath)
232	if !ok {
233		return false
234	}
235	if AddressNamespace(ns) {
236		return ns == c.Claimant.String()
237	}
238	if resolve == nil {
239		return false
240	}
241	addr, held := resolve(ns)
242	return held && addr == c.Claimant
243}
244
245// SourceURL builds a browsable link to the claimed source: the commit, plus the
246// directory when the package is not at the repository root.
247//
248// GitHub's "/tree/<commit>/<dir>" layout is also GitLab's, Gitea's, Forgejo's
249// and Codeberg's, so one construction covers every forge this registry is
250// likely to meet. It is a convenience, not a promise the link resolves: the
251// repository may be gone, private or renamed since the claim was made, which is
252// itself something a verifier reports rather than something a realm can know.
253func (c *Claim) SourceURL() string {
254	u := strings.TrimSuffix(c.Repo, "/") + "/tree/" + c.Commit
255	if c.Dir != "" {
256		u += "/" + c.Dir
257	}
258	return u
259}