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

upgradeable.gno

17.20 Kb · 535 lines
  1// Package upgradeable lets a realm at a permanent path serve behavior that
  2// can change.
  3//
  4// A gno.land package path is immutable. The VM keeper refuses MsgAddPackage
  5// at a path that already holds a package, and the one exception -- private
  6// = true in gnomod.toml -- buys redeployability by giving up importability,
  7// and starts the realm's globals over from nothing besides. So a realm other
  8// people import cannot have its code replaced. What it can do is decide at
  9// call time which object it calls, and that object can live in a realm
 10// deployed years later. There is no delegatecall here and imports resolve
 11// statically: the indirection is an ordinary interface value, handed over by
 12// the realm that implements it.
 13//
 14// [Proxy] is the bookkeeping for that. It holds the one live implementation,
 15// the candidates waiting to replace it, and the ones it used to be, with an
 16// [Authority] deciding who may move between them. What it deliberately does
 17// not hold is your state. State belongs in the realm at the permanent path,
 18// or in a realm of its own, so that replacing an implementation does not
 19// touch it -- see the counter example under r/ for both halves.
 20//
 21// This is a p/ package on purpose. Pure packages can never be redeployed, so
 22// the rules below cannot be swapped out from under the realm that relies on
 23// them, which is exactly the property an upgrade mechanism has to have.
 24//
 25// # Threading cur
 26//
 27// Every authority-sensitive method takes (_ int, rlm realm): the caller
 28// threads its own cur as data instead of crossing into this package. The
 29// leading int keeps rlm out of first position, where it would declare a
 30// crossing function. rlm.Previous() is then the realm that crossed into the
 31// caller -- the user sending the transaction, the governance realm executing
 32// a proposal, or the implementation realm registering itself -- and
 33// rlm.IsCurrent() is what makes that unforgeable: a realm value cannot be
 34// persisted, and a stale capture fails the check.
 35package upgradeable
 36
 37import (
 38	"chain"
 39	"chain/runtime"
 40	"strings"
 41)
 42
 43// Event types emitted on every state change, so an indexer can reconstruct
 44// which code served which block without replaying the realm.
 45const (
 46	ProposeEvent   = "UpgradeProposed"
 47	WithdrawEvent  = "UpgradeWithdrawn"
 48	AcceptEvent    = "UpgradeAccepted"
 49	RollbackEvent  = "UpgradeRolledBack"
 50	ForgetEvent    = "UpgradeHistoryDropped"
 51	ExtendEvent    = "UpgradeExtensionAdded"
 52	UnextendEvent  = "UpgradeExtensionDropped"
 53	FreezeEvent    = "UpgradeFrozen"
 54	AuthorityEvent = "UpgradeAuthorityTransferred"
 55)
 56
 57// Release is one implementation: the object, and the realm that authored it.
 58//
 59// PkgPath is read off the crossing frame at registration, never taken as an
 60// argument, so it names the realm the code actually lives at. That is what
 61// makes it worth reviewing before an upgrade is accepted, and what lets a
 62// state realm recognize the live implementation later.
 63//
 64// Release is returned by value. The proxy's own copy stays unreachable from
 65// outside, so a reader cannot rewrite history.
 66type Release struct {
 67	pkgPath string
 68	impl    any
 69	height  int64
 70}
 71
 72// PkgPath returns the realm path the implementation was registered from.
 73func (r Release) PkgPath() string { return r.pkgPath }
 74
 75// Impl returns the implementation object.
 76func (r Release) Impl() any { return r.impl }
 77
 78// Height returns the block height at which the implementation registered.
 79func (r Release) Height() int64 { return r.height }
 80
 81// IsZero reports whether r is the zero Release, which is what accessors
 82// return alongside a false ok.
 83func (r Release) IsZero() bool { return r.pkgPath == "" }
 84
 85// Proxy holds the live implementation and the bookkeeping around replacing
 86// it. Keep one as a realm-level variable; it persists with the realm.
 87type Proxy struct {
 88	auth    Authority
 89	live    *Release
 90	pending []*Release // kept sorted by pkgPath
 91	past     []*Release
 92	exts     []string // kept sorted; realms allowed to act as this one
 93	frozen   bool
 94	nested   bool
 95}
 96
 97// New returns a Proxy whose candidates must register from a realm nested
 98// under the realm holding it: gno.land/r/you/app/impl/v1 under
 99// gno.land/r/you/app. Deploying under that prefix needs your namespace, so
100// the pending set cannot be filled by strangers.
101//
102// It panics on a nil authority. Upgradeability with no authority is not
103// "nobody can upgrade" -- to reach that, Freeze.
104func New(auth Authority) *Proxy {
105	return newProxy(auth, true)
106}
107
108// NewOpen is New without the nesting rule: any deployed realm may register a
109// candidate and the authority alone decides. Use it when implementations
110// come from outside your namespace. The cost is that anyone can add entries
111// to the pending set, which lives in your realm's storage; Withdraw clears
112// them.
113func NewOpen(auth Authority) *Proxy {
114	return newProxy(auth, false)
115}
116
117func newProxy(auth Authority, nested bool) *Proxy {
118	if auth == nil {
119		panic(ErrNoAuthority)
120	}
121	return &Proxy{
122		auth:   auth,
123		nested: nested,
124	}
125}
126
127// Impl returns the live implementation, and panics with ErrNoImpl if nothing
128// has been accepted yet. Realms call this on every request, so the panic is
129// the honest answer: a proxy with no implementation has no behavior to serve.
130func (p *Proxy) Impl() any {
131	if p.live == nil {
132		panic(ErrNoImpl)
133	}
134	return p.live.impl
135}
136
137// TryImpl returns the live implementation and whether there is one, for
138// callers that would rather render an empty page than fail the transaction.
139func (p *Proxy) TryImpl() (any, bool) {
140	if p.live == nil {
141		return nil, false
142	}
143	return p.live.impl, true
144}
145
146// Live returns the live release.
147func (p *Proxy) Live() (Release, bool) {
148	if p.live == nil {
149		return Release{}, false
150	}
151	return *p.live, true
152}
153
154// LivePath returns the realm path of the live implementation, or "" if there
155// is none. A state realm that wants to grant write access to whichever
156// implementation is current compares its caller against this.
157func (p *Proxy) LivePath() string {
158	if p.live == nil {
159		return ""
160	}
161	return p.live.pkgPath
162}
163
164// Pending returns the registered candidates, ordered by path.
165func (p *Proxy) Pending() []Release {
166	out := make([]Release, 0, len(p.pending))
167	for _, r := range p.pending {
168		out = append(out, *r)
169	}
170	return out
171}
172
173// History returns the releases this proxy has already served, oldest first.
174// It excludes the live one, and Forget empties it.
175func (p *Proxy) History() []Release {
176	out := make([]Release, 0, len(p.past))
177	for _, r := range p.past {
178		out = append(out, *r)
179	}
180	return out
181}
182
183// Frozen reports whether upgradeability has ended.
184func (p *Proxy) Frozen() bool { return p.frozen }
185
186// Authority returns the current authority.
187func (p *Proxy) Authority() Authority { return p.auth }
188
189// Extensions returns the realm paths allowed to act as the realm holding this
190// proxy, in sorted order.
191//
192// A realm path is fixed, so the exported functions of the realm holding this
193// proxy can never grow. What can grow is the set of OTHER realms permitted to
194// run against its state: a realm deployed later declares an interface of its
195// own, asserts the live implementation to it, and calls through. That is how
196// an application adds an entry point it did not ship with. See the
197// "extending a frozen API" pattern in the repository docs.
198//
199// The state accessor in the realm holding this proxy is what consults this --
200// nothing here reaches into your state on its own.
201func (p *Proxy) Extensions() []string {
202	dup := make([]string, len(p.exts))
203	copy(dup, p.exts)
204	return dup
205}
206
207// IsExtension reports whether pkgPath may act as the realm holding this proxy.
208// Call it from the state accessor, alongside the check for the realm's own
209// path.
210func (p *Proxy) IsExtension(pkgPath string) bool {
211	if pkgPath == "" {
212		return false // a user call is not a realm
213	}
214	for _, e := range p.exts {
215		if e == pkgPath {
216			return true
217		}
218	}
219	return false
220}
221
222// AddExtension permits pkgPath to act as the realm holding this proxy.
223//
224// This grants the authority no power it did not have. An authority that can
225// Accept an arbitrary implementation can already run arbitrary code against
226// the state; naming a second realm that may do the same widens what is
227// reachable, not who decides. It does widen the surface a reviewer has to
228// read, which is why Extensions is public and Render should show it.
229//
230// Caller: the authority.
231func (p *Proxy) AddExtension(_ int, rlm realm, pkgPath string) {
232	p.assertMutable()
233	p.assertAuthorized(0, rlm)
234	if pkgPath == "" || pkgPath != strings.TrimSpace(pkgPath) {
235		panic(ErrBadRealmPath)
236	}
237	if p.IsExtension(pkgPath) {
238		return
239	}
240	at := len(p.exts)
241	for i, e := range p.exts {
242		if pkgPath < e {
243			at = i
244			break
245		}
246	}
247	p.exts = append(p.exts, "")
248	copy(p.exts[at+1:], p.exts[at:])
249	p.exts[at] = pkgPath
250	chain.Emit(ExtendEvent, "realm", pkgPath)
251}
252
253// DropExtension revokes pkgPath. The extension realm stays deployed and keeps
254// answering calls; it just stops being able to reach the state, so whatever it
255// exposed starts panicking rather than disappearing.
256//
257// Caller: the authority.
258func (p *Proxy) DropExtension(_ int, rlm realm, pkgPath string) {
259	p.assertMutable()
260	p.assertAuthorized(0, rlm)
261	for i, e := range p.exts {
262		if e == pkgPath {
263			p.exts = append(p.exts[:i], p.exts[i+1:]...)
264			chain.Emit(UnextendEvent, "realm", pkgPath)
265			return
266		}
267	}
268	panic(ErrUnknownPath)
269}
270
271// AssertAuthorized panics unless rlm's caller is the authority. It exists for
272// wrappers that do work of their own before delegating -- a schema check
273// before Accept, say -- so that an unauthorized caller is refused for the
274// right reason rather than tripping over the wrapper's own validation first.
275func (p *Proxy) AssertAuthorized(_ int, rlm realm) { p.assertAuthorized(0, rlm) }
276
277// Propose registers impl as a candidate, filed under the path of the realm
278// that is calling.
279//
280// That path comes off the crossing frame rather than from an argument, and
281// that is the point of the whole mechanism: a candidate cannot claim to have
282// been authored by a path it does not occupy. Whoever accepts it is
283// therefore accepting code they can go and read at that path, and a state
284// realm that gates writes on LivePath is gating on the same authenticated
285// string.
286//
287// The implementation realm calls this from its own init, so deploying the
288// new version is what nominates it. Nothing is served until the authority
289// accepts. Re-registering from the same realm replaces that candidate, which
290// is the retry path after a failed deployment.
291func (p *Proxy) Propose(_ int, rlm realm, impl any) {
292	p.assertMutable()
293	if !rlm.IsCurrent() {
294		panic(ErrStaleRealm)
295	}
296	if impl == nil {
297		panic(ErrNilImpl)
298	}
299
300	prev := rlm.Previous()
301	from := prev.PkgPath()
302	if from == "" || prev.IsEphemeral() {
303		// A user call has no path to record at all. An ephemeral one does
304		// have a path -- `gnokey maketx run` executes in /e/<addr>/run --
305		// but it does not outlive the transaction, so filing a candidate
306		// under it records a path that will never hold the code it claims.
307		// The emptiness check alone does not catch that one.
308		panic(ErrNotARealm)
309	}
310	if p.nested && !isNested(rlm.PkgPath(), from) {
311		panic(ErrNotNested)
312	}
313
314	p.setPending(&Release{
315		pkgPath: from,
316		impl:    impl,
317		height:  runtime.ChainHeight(),
318	})
319	chain.Emit(ProposeEvent, "impl", from)
320}
321
322// Withdraw drops a candidate. The authority may drop any; a realm may always
323// drop its own, which is how a superseded candidate stops costing storage
324// without the authority having to act.
325func (p *Proxy) Withdraw(_ int, rlm realm, pkgPath string) {
326	p.assertMutable()
327	if !rlm.IsCurrent() {
328		panic(ErrStaleRealm)
329	}
330	if rlm.Previous().PkgPath() != pkgPath {
331		p.assertAuthorized(0, rlm)
332	}
333	if !p.removePending(pkgPath) {
334		panic(ErrUnknownPath)
335	}
336	chain.Emit(WithdrawEvent, "impl", pkgPath)
337}
338
339// Accept makes the candidate at pkgPath the live implementation, and files
340// the one it replaces in History.
341//
342// This is the upgrade. It is a separate transaction from the deployment that
343// registered the candidate, by a separate principal, which is what gives the
344// authority something reviewable to act on -- the same two-phase shape
345// gno.land itself uses when a chain parks a submission until an approver
346// enables it.
347func (p *Proxy) Accept(_ int, rlm realm, pkgPath string) {
348	p.assertMutable()
349	p.assertAuthorized(0, rlm)
350
351	i := p.findPending(pkgPath)
352	if i < 0 {
353		panic(ErrUnknownPath)
354	}
355	rel := p.pending[i]
356	p.removePending(pkgPath)
357
358	from := ""
359	if p.live != nil {
360		from = p.live.pkgPath
361		p.past = append(p.past, p.live)
362	}
363	p.live = rel
364	chain.Emit(AcceptEvent, "impl", pkgPath, "replaced", from)
365}
366
367// Rollback puts the previous release back in front, and returns the one it
368// displaces to the pending set so a fixed version can be re-accepted without
369// redeploying it.
370//
371// This is why History is kept: the release objects stay reachable, and
372// reachable is the only form of "still deployed" that matters here. It costs
373// storage, which is what Forget is for once a version has proven itself.
374func (p *Proxy) Rollback(_ int, rlm realm) {
375	p.assertMutable()
376	p.assertAuthorized(0, rlm)
377
378	n := len(p.past)
379	if n == 0 {
380		panic(ErrNoHistory)
381	}
382	prev := p.past[n-1]
383	p.past = p.past[:n-1]
384
385	from := ""
386	if p.live != nil {
387		from = p.live.pkgPath
388		p.setPending(p.live)
389	}
390	p.live = prev
391	chain.Emit(RollbackEvent, "impl", prev.pkgPath, "replaced", from)
392}
393
394// Forget drops the history, releasing the old implementations. Rollback
395// stops working -- there is nothing to roll back to -- so this is the "the
396// current version has proven itself" move, not routine cleanup.
397func (p *Proxy) Forget(_ int, rlm realm) {
398	p.assertMutable()
399	p.assertAuthorized(0, rlm)
400	dropped := len(p.past)
401	p.past = nil
402	chain.Emit(ForgetEvent, "dropped", itoa(dropped))
403}
404
405// TransferAuthority hands the power to upgrade to auth. It panics on nil:
406// there is no way to leave a proxy upgradeable by nobody, because that state
407// is indistinguishable from a mistake. Freeze says it on purpose.
408func (p *Proxy) TransferAuthority(_ int, rlm realm, auth Authority) {
409	p.assertMutable()
410	p.assertAuthorized(0, rlm)
411	if auth == nil {
412		panic(ErrNoAuthority)
413	}
414	old := p.auth.String()
415	p.auth = auth
416	chain.Emit(AuthorityEvent, "from", old, "to", auth.String())
417}
418
419// Freeze ends upgradeability for good. The live implementation keeps serving
420// and nothing can replace it: no unfreeze, by design, because a proxy that
421// can be thawed has not actually given anything up. This is how a realm
422// graduates from "we are still fixing it" to something other realms can
423// build on -- an interrealm contract is only as trustworthy as the least
424// mutable realm behind it.
425//
426// It refuses to freeze with nothing live, which would leave the realm
427// permanently unable to answer a call.
428//
429// Freezing finalizes the extension set. Whatever is registered survives (the
430// frozen implementation may depend on it), but afterwards nothing can be added
431// and -- because Drop is a mutation too -- nothing can be dropped. So a
432// registered extension keeps its access to the realm's state for good. Review
433// the extensions before you freeze; you cannot revoke one after.
434func (p *Proxy) Freeze(_ int, rlm realm) {
435	p.assertMutable()
436	p.assertAuthorized(0, rlm)
437	if p.live == nil {
438		panic(ErrNoImpl)
439	}
440	p.frozen = true
441	p.pending = nil
442	p.past = nil
443	// Extensions are deliberately kept. They are reachable code that the
444	// frozen implementation may depend on, and dropping them here would
445	// break entry points that were working a block ago -- a freeze should
446	// fix the realm's behavior, not change it.
447	chain.Emit(FreezeEvent, "impl", p.live.pkgPath)
448}
449
450// isNested reports whether child is parent, or sits under it. The trailing
451// separators are what keep gno.land/r/you/appliance from counting as nested
452// under gno.land/r/you/app.
453func isNested(parent, child string) bool {
454	return strings.HasPrefix(child+"/", parent+"/")
455}
456
457// findPending returns the index of the candidate at pkgPath, or -1.
458func (p *Proxy) findPending(pkgPath string) int {
459	for i, r := range p.pending {
460		if r.pkgPath == pkgPath {
461			return i
462		}
463	}
464	return -1
465}
466
467// setPending inserts rel, or replaces the candidate already at its path.
468// The slice is kept sorted so Pending and any Render built on it read the
469// same way on every node.
470func (p *Proxy) setPending(rel *Release) {
471	if i := p.findPending(rel.pkgPath); i >= 0 {
472		p.pending[i] = rel
473		return
474	}
475	at := len(p.pending)
476	for i, r := range p.pending {
477		if rel.pkgPath < r.pkgPath {
478			at = i
479			break
480		}
481	}
482	p.pending = append(p.pending, nil)
483	copy(p.pending[at+1:], p.pending[at:])
484	p.pending[at] = rel
485}
486
487// removePending drops the candidate at pkgPath, reporting whether one was
488// there.
489func (p *Proxy) removePending(pkgPath string) bool {
490	i := p.findPending(pkgPath)
491	if i < 0 {
492		return false
493	}
494	p.pending = append(p.pending[:i], p.pending[i+1:]...)
495	return true
496}
497
498func (p *Proxy) assertMutable() {
499	if p.frozen {
500		panic(ErrFrozen)
501	}
502}
503
504func (p *Proxy) assertAuthorized(_ int, rlm realm) {
505	if !rlm.IsCurrent() {
506		panic(ErrStaleRealm)
507	}
508	caller := rlm.Previous()
509	if !p.auth.Authorized(caller.Address(), caller.PkgPath()) {
510		panic(ErrUnauthorized)
511	}
512}
513
514// itoa avoids pulling ufmt in for one event attribute.
515func itoa(n int) string {
516	if n == 0 {
517		return "0"
518	}
519	neg := n < 0
520	if neg {
521		n = -n
522	}
523	var buf [20]byte
524	i := len(buf)
525	for n > 0 {
526		i--
527		buf[i] = byte('0' + n%10)
528		n /= 10
529	}
530	if neg {
531		i--
532		buf[i] = '-'
533	}
534	return string(buf[i:])
535}