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}