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.

upgradeable.gno

17.19 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}