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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}