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

v0 source pure

Package upgradeable lets a realm at a permanent path serve behavior that can change.

Overview

Package upgradeable lets a realm at a permanent path serve behavior that can change.

A gno.land package path is immutable. The VM keeper refuses MsgAddPackage at a path that already holds a package, and the one exception -- private = true in gnomod.toml -- buys redeployability by giving up importability, and starts the realm's globals over from nothing besides. So a realm other people import cannot have its code replaced. What it can do is decide at call time which object it calls, and that object can live in a realm deployed years later. There is no delegatecall here and imports resolve statically: the indirection is an ordinary interface value, handed over by the realm that implements it.

Proxy is the bookkeeping for that. It holds the one live implementation, the candidates waiting to replace it, and the ones it used to be, with an Authority deciding who may move between them. What it deliberately does not hold is your state. State belongs in the realm at the permanent path, or in a realm of its own, so that replacing an implementation does not touch it -- see the counter example under r/ for both halves.

This is a p/ package on purpose. Pure packages can never be redeployed, so the rules below cannot be swapped out from under the realm that relies on them, which is exactly the property an upgrade mechanism has to have.

Threading cur

Every authority-sensitive method takes (_ int, rlm realm): the caller threads its own cur as data instead of crossing into this package. The leading int keeps rlm out of first position, where it would declare a crossing function. rlm.Previous() is then the realm that crossed into the caller -- the user sending the transaction, the governance realm executing a proposal, or the implementation realm registering itself -- and rlm.IsCurrent() is what makes that unforgeable: a realm value cannot be persisted, and a stale capture fails the check.

Constants 1

const ProposeEvent, WithdrawEvent, AcceptEvent, RollbackEvent, ForgetEvent, ExtendEvent, UnextendEvent, FreezeEvent, AuthorityEvent

 1const (
 2	ProposeEvent   = "UpgradeProposed"
 3	WithdrawEvent  = "UpgradeWithdrawn"
 4	AcceptEvent    = "UpgradeAccepted"
 5	RollbackEvent  = "UpgradeRolledBack"
 6	ForgetEvent    = "UpgradeHistoryDropped"
 7	ExtendEvent    = "UpgradeExtensionAdded"
 8	UnextendEvent  = "UpgradeExtensionDropped"
 9	FreezeEvent    = "UpgradeFrozen"
10	AuthorityEvent = "UpgradeAuthorityTransferred"
11)
source

Event types emitted on every state change, so an indexer can reconstruct which code served which block without replaying the realm.

Variables 1

var ErrNoAuthority, ErrUnauthorized, ErrStaleRealm, ErrFrozen, ErrNoImpl, ErrNotNested, ErrNotARealm, ErrNilImpl, ErrUnknownPath, ErrNoHistory, ErrBadAddress, ErrBadRealmPath

 1var (
 2	// ErrNoAuthority is raised when a Proxy is built, or handed, a nil
 3	// authority. There is no "nobody" authority: to end upgradeability,
 4	// Freeze it.
 5	ErrNoAuthority = errors.New("upgradeable: an authority is required")
 6
 7	// ErrUnauthorized is raised when the caller is not the authority.
 8	ErrUnauthorized = errors.New("upgradeable: caller is not the authority")
 9
10	// ErrStaleRealm is raised when the threaded realm value is not the
11	// caller's live cur -- a stashed or replayed capture.
12	ErrStaleRealm = errors.New("upgradeable: realm value is not the caller's live cur")
13
14	// ErrFrozen is raised by every mutating method once Freeze has run.
15	ErrFrozen = errors.New("upgradeable: proxy is frozen")
16
17	// ErrNoImpl is raised when the proxy is asked for an implementation it
18	// does not have yet.
19	ErrNoImpl = errors.New("upgradeable: no implementation is live")
20
21	// ErrNotNested is raised when a candidate registers from a realm that is
22	// not under the proxy realm's own path. See New and NewOpen.
23	ErrNotNested = errors.New("upgradeable: candidate realm is not nested under this one")
24
25	// ErrNotARealm is raised when a candidate registers from a user call
26	// rather than from a deployed realm, so there is no path to record.
27	ErrNotARealm = errors.New("upgradeable: only a deployed realm can register a candidate")
28
29	// ErrNilImpl is raised when the registered implementation is nil.
30	ErrNilImpl = errors.New("upgradeable: implementation is nil")
31
32	// ErrUnknownPath is raised when accepting or withdrawing a path that has
33	// no candidate registered against it.
34	ErrUnknownPath = errors.New("upgradeable: no candidate registered at that path")
35
36	// ErrNoHistory is raised by Rollback when nothing has been replaced yet.
37	ErrNoHistory = errors.New("upgradeable: no previous release to roll back to")
38
39	// ErrBadAddress is raised when an authority is built from an invalid
40	// address.
41	ErrBadAddress = errors.New("upgradeable: invalid address")
42
43	// ErrBadRealmPath is raised when an authority is built from a realm path
44	// that is blank or carries surrounding whitespace. Both are silent
45	// lockouts: a blank entry matches every user call, and a padded one
46	// matches no caller at all.
47	ErrBadRealmPath = errors.New("upgradeable: authority realm paths must be non-blank and unpadded")
48)
source

Functions 5

func NewAddrAuthority

1func NewAddrAuthority(addr address) *AddrAuthority
source

NewAddrAuthority returns an Authority holding addr.

func NewAnyOf

1func NewAnyOf(auths ...Authority) *AnyOf
source

NewAnyOf returns an Authority satisfied by any of auths. It panics on an empty list or a nil member, both of which would silently weaken or void the check.

func New

1func New(auth Authority) *Proxy
source

New returns a Proxy whose candidates must register from a realm nested under the realm holding it: gno.land/r/you/app/impl/v1 under gno.land/r/you/app. Deploying under that prefix needs your namespace, so the pending set cannot be filled by strangers.

It panics on a nil authority. Upgradeability with no authority is not "nobody can upgrade" -- to reach that, Freeze.

func NewOpen

1func NewOpen(auth Authority) *Proxy
source

NewOpen is New without the nesting rule: any deployed realm may register a candidate and the authority alone decides. Use it when implementations come from outside your namespace. The cost is that anyone can add entries to the pending set, which lives in your realm's storage; Withdraw clears them.

func NewRealmAuthority

1func NewRealmAuthority(paths ...string) *RealmAuthority
source

NewRealmAuthority returns an Authority holding paths. It panics on an empty list rather than authorizing nobody, and on a blank or padded entry rather than storing one that can never match a caller -- or, for a blank one, matches every user call.

Types 6

type AddrAuthority

struct
1type AddrAuthority struct {
2	addr address
3}
source

AddrAuthority authorizes exactly one address: the signer of a direct call, or a realm acting at that address.

Methods on AddrAuthority

func Address

method on AddrAuthority
1func (a *AddrAuthority) Address() address
source

Address returns the authorized address.

func Authorized

method on AddrAuthority
1func (a *AddrAuthority) Authorized(addr address, _ string) bool
source

func String

method on AddrAuthority
1func (a *AddrAuthority) String() string
source

type AnyOf

struct
1type AnyOf struct {
2	auths []Authority
3}
source

AnyOf authorizes a caller that any of its members authorizes. It is the handover shape for the common case: hold an address authority while you are still deploying, add the governance realm, drop the address later.

Methods on AnyOf

func Authorized

method on AnyOf
1func (a *AnyOf) Authorized(addr address, pkgPath string) bool
source

func String

method on AnyOf
1func (a *AnyOf) String() string
source

type Authority

interface
1type Authority interface {
2	Authorized(addr address, pkgPath string) bool
3	String() string
4}
source

Authority decides who may change what a Proxy points at.

It is handed the identity of the caller -- the realm that crossed into the realm holding the Proxy. When a transaction is sent straight to that realm the caller is a user: pkgPath is empty and addr is the signer. When the call arrives through another realm, say a DAO executing a proposal, pkgPath names that realm and addr is the realm's own address.

Both halves are passed because the two authorization styles on gno.land read different ones: an owner check compares addresses, a governance check compares realm paths. An implementation must read the half it means and ignore the other. In particular an empty pkgPath is not a realm -- it is every user call -- so a path check must reject it explicitly.

type Proxy

struct
1type Proxy struct {
2	auth    Authority
3	live    *Release
4	pending []*Release // kept sorted by pkgPath
5	past    []*Release
6	exts    []string // kept sorted; realms allowed to act as this one
7	frozen  bool
8	nested  bool
9}
source

Proxy holds the live implementation and the bookkeeping around replacing it. Keep one as a realm-level variable; it persists with the realm.

Methods on Proxy

func Accept

method on Proxy
1func (p *Proxy) Accept(_ int, rlm realm, pkgPath string)
source

Accept makes the candidate at pkgPath the live implementation, and files the one it replaces in History.

This is the upgrade. It is a separate transaction from the deployment that registered the candidate, by a separate principal, which is what gives the authority something reviewable to act on -- the same two-phase shape gno.land itself uses when a chain parks a submission until an approver enables it.

func AddExtension

method on Proxy
1func (p *Proxy) AddExtension(_ int, rlm realm, pkgPath string)
source

AddExtension permits pkgPath to act as the realm holding this proxy.

This grants the authority no power it did not have. An authority that can Accept an arbitrary implementation can already run arbitrary code against the state; naming a second realm that may do the same widens what is reachable, not who decides. It does widen the surface a reviewer has to read, which is why Extensions is public and Render should show it.

Caller: the authority.

func AssertAuthorized

method on Proxy
1func (p *Proxy) AssertAuthorized(_ int, rlm realm)
source

AssertAuthorized panics unless rlm's caller is the authority. It exists for wrappers that do work of their own before delegating -- a schema check before Accept, say -- so that an unauthorized caller is refused for the right reason rather than tripping over the wrapper's own validation first.

func Authority

method on Proxy
1func (p *Proxy) Authority() Authority
source

Authority returns the current authority.

func DropExtension

method on Proxy
1func (p *Proxy) DropExtension(_ int, rlm realm, pkgPath string)
source

DropExtension revokes pkgPath. The extension realm stays deployed and keeps answering calls; it just stops being able to reach the state, so whatever it exposed starts panicking rather than disappearing.

Caller: the authority.

func Extensions

method on Proxy
1func (p *Proxy) Extensions() []string
source

Extensions returns the realm paths allowed to act as the realm holding this proxy, in sorted order.

A realm path is fixed, so the exported functions of the realm holding this proxy can never grow. What can grow is the set of OTHER realms permitted to run against its state: a realm deployed later declares an interface of its own, asserts the live implementation to it, and calls through. That is how an application adds an entry point it did not ship with. See the "extending a frozen API" pattern in the repository docs.

The state accessor in the realm holding this proxy is what consults this -- nothing here reaches into your state on its own.

func Forget

method on Proxy
1func (p *Proxy) Forget(_ int, rlm realm)
source

Forget drops the history, releasing the old implementations. Rollback stops working -- there is nothing to roll back to -- so this is the "the current version has proven itself" move, not routine cleanup.

func Freeze

method on Proxy
1func (p *Proxy) Freeze(_ int, rlm realm)
source

Freeze ends upgradeability for good. The live implementation keeps serving and nothing can replace it: no unfreeze, by design, because a proxy that can be thawed has not actually given anything up. This is how a realm graduates from "we are still fixing it" to something other realms can build on -- an interrealm contract is only as trustworthy as the least mutable realm behind it.

It refuses to freeze with nothing live, which would leave the realm permanently unable to answer a call.

Freezing finalizes the extension set. Whatever is registered survives (the frozen implementation may depend on it), but afterwards nothing can be added and -- because Drop is a mutation too -- nothing can be dropped. So a registered extension keeps its access to the realm's state for good. Review the extensions before you freeze; you cannot revoke one after.

func Frozen

method on Proxy
1func (p *Proxy) Frozen() bool
source

Frozen reports whether upgradeability has ended.

func History

method on Proxy
1func (p *Proxy) History() []Release
source

History returns the releases this proxy has already served, oldest first. It excludes the live one, and Forget empties it.

func Impl

method on Proxy
1func (p *Proxy) Impl() any
source

Impl returns the live implementation, and panics with ErrNoImpl if nothing has been accepted yet. Realms call this on every request, so the panic is the honest answer: a proxy with no implementation has no behavior to serve.

func IsExtension

method on Proxy
1func (p *Proxy) IsExtension(pkgPath string) bool
source

IsExtension reports whether pkgPath may act as the realm holding this proxy. Call it from the state accessor, alongside the check for the realm's own path.

func Live

method on Proxy
1func (p *Proxy) Live() (Release, bool)
source

Live returns the live release.

func LivePath

method on Proxy
1func (p *Proxy) LivePath() string
source

LivePath returns the realm path of the live implementation, or "" if there is none. A state realm that wants to grant write access to whichever implementation is current compares its caller against this.

func Pending

method on Proxy
1func (p *Proxy) Pending() []Release
source

Pending returns the registered candidates, ordered by path.

func Propose

method on Proxy
1func (p *Proxy) Propose(_ int, rlm realm, impl any)
source

Propose registers impl as a candidate, filed under the path of the realm that is calling.

That path comes off the crossing frame rather than from an argument, and that is the point of the whole mechanism: a candidate cannot claim to have been authored by a path it does not occupy. Whoever accepts it is therefore accepting code they can go and read at that path, and a state realm that gates writes on LivePath is gating on the same authenticated string.

The implementation realm calls this from its own init, so deploying the new version is what nominates it. Nothing is served until the authority accepts. Re-registering from the same realm replaces that candidate, which is the retry path after a failed deployment.

func Rollback

method on Proxy
1func (p *Proxy) Rollback(_ int, rlm realm)
source

Rollback puts the previous release back in front, and returns the one it displaces to the pending set so a fixed version can be re-accepted without redeploying it.

This is why History is kept: the release objects stay reachable, and reachable is the only form of "still deployed" that matters here. It costs storage, which is what Forget is for once a version has proven itself.

func TransferAuthority

method on Proxy
1func (p *Proxy) TransferAuthority(_ int, rlm realm, auth Authority)
source

TransferAuthority hands the power to upgrade to auth. It panics on nil: there is no way to leave a proxy upgradeable by nobody, because that state is indistinguishable from a mistake. Freeze says it on purpose.

func TryImpl

method on Proxy
1func (p *Proxy) TryImpl() (any, bool)
source

TryImpl returns the live implementation and whether there is one, for callers that would rather render an empty page than fail the transaction.

func Withdraw

method on Proxy
1func (p *Proxy) Withdraw(_ int, rlm realm, pkgPath string)
source

Withdraw drops a candidate. The authority may drop any; a realm may always drop its own, which is how a superseded candidate stops costing storage without the authority having to act.

type RealmAuthority

struct
1type RealmAuthority struct {
2	paths []string
3}
source

RealmAuthority authorizes a fixed set of realm paths, the shape r/gov/dao uses: a governance realm executes the upgrade, and the proxy recognizes it by the path it was deployed at rather than by an address.

More than one path is allowed so that authority can be moved without a gap: list both the old and the new governance realm, let the new one take over, then narrow the list.

Methods on RealmAuthority

func Authorized

method on RealmAuthority
1func (a *RealmAuthority) Authorized(_ address, pkgPath string) bool
source

func Paths

method on RealmAuthority
1func (a *RealmAuthority) Paths() []string
source

Paths returns a copy of the authorized realm paths.

func String

method on RealmAuthority
1func (a *RealmAuthority) String() string
source

type Release

struct
1type Release struct {
2	pkgPath string
3	impl    any
4	height  int64
5}
source

Release is one implementation: the object, and the realm that authored it.

PkgPath is read off the crossing frame at registration, never taken as an argument, so it names the realm the code actually lives at. That is what makes it worth reviewing before an upgrade is accepted, and what lets a state realm recognize the live implementation later.

Release is returned by value. The proxy's own copy stays unreachable from outside, so a reader cannot rewrite history.

Methods on Release

func Height

method on Release
1func (r Release) Height() int64
source

Height returns the block height at which the implementation registered.

func Impl

method on Release
1func (r Release) Impl() any
source

Impl returns the implementation object.

func IsZero

method on Release
1func (r Release) IsZero() bool
source

IsZero reports whether r is the zero Release, which is what accessors return alongside a false ok.

func PkgPath

method on Release
1func (r Release) PkgPath() string
source

PkgPath returns the realm path the implementation was registered from.

Imports 4

  • chain stdlib
  • chain/runtime stdlib
  • errors stdlib
  • strings stdlib

Source Files 4