1// Package upgradeable lets a realm at a permanent path serve behavior that2// can change.3//4// A gno.land package path is immutable. The VM keeper refuses MsgAddPackage5// at a path that already holds a package, and the one exception -- private6// = true in gnomod.toml -- buys redeployability by giving up importability,7// and starts the realm's globals over from nothing besides. So a realm other8// people import cannot have its code replaced. What it can do is decide at9// call time which object it calls, and that object can live in a realm10// deployed years later. There is no delegatecall here and imports resolve11// statically: the indirection is an ordinary interface value, handed over by12// 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 an16// [Authority] deciding who may move between them. What it deliberately does17// 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 not19// 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, so22// the rules below cannot be swapped out from under the realm that relies on23// them, which is exactly the property an upgrade mechanism has to have.24//25// # Threading cur26//27// Every authority-sensitive method takes (_ int, rlm realm): the caller28// threads its own cur as data instead of crossing into this package. The29// leading int keeps rlm out of first position, where it would declare a30// crossing function. rlm.Previous() is then the realm that crossed into the31// caller -- the user sending the transaction, the governance realm executing32// a proposal, or the implementation realm registering itself -- and33// rlm.IsCurrent() is what makes that unforgeable: a realm value cannot be34// persisted, and a stale capture fails the check.35package upgradeable3637import (38 "chain"39 "chain/runtime"40 "strings"41)4243// Event types emitted on every state change, so an indexer can reconstruct44// 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)5657// 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 an60// argument, so it names the realm the code actually lives at. That is what61// makes it worth reviewing before an upgrade is accepted, and what lets a62// state realm recognize the live implementation later.63//64// Release is returned by value. The proxy's own copy stays unreachable from65// outside, so a reader cannot rewrite history.66type Release struct {67 pkgPath string68 impl any69 height int6470}7172// PkgPath returns the realm path the implementation was registered from.73func (r Release) PkgPath() string { return r.pkgPath }7475// Impl returns the implementation object.76func (r Release) Impl() any { return r.impl }7778// Height returns the block height at which the implementation registered.79func (r Release) Height() int64 { return r.height }8081// IsZero reports whether r is the zero Release, which is what accessors82// return alongside a false ok.83func (r Release) IsZero() bool { return r.pkgPath == "" }8485// Proxy holds the live implementation and the bookkeeping around replacing86// it. Keep one as a realm-level variable; it persists with the realm.87type Proxy struct {88 auth Authority89 live *Release90 pending []*Release // kept sorted by pkgPath91 past []*Release92 exts []string // kept sorted; realms allowed to act as this one93 frozen bool94 nested bool95}9697// New returns a Proxy whose candidates must register from a realm nested98// under the realm holding it: gno.land/r/you/app/impl/v1 under99// gno.land/r/you/app. Deploying under that prefix needs your namespace, so100// the pending set cannot be filled by strangers.101//102// It panics on a nil authority. Upgradeability with no authority is not103// "nobody can upgrade" -- to reach that, Freeze.104func New(auth Authority) *Proxy {105 return newProxy(auth, true)106}107108// NewOpen is New without the nesting rule: any deployed realm may register a109// candidate and the authority alone decides. Use it when implementations110// come from outside your namespace. The cost is that anyone can add entries111// to the pending set, which lives in your realm's storage; Withdraw clears112// them.113func NewOpen(auth Authority) *Proxy {114 return newProxy(auth, false)115}116117func 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}126127// Impl returns the live implementation, and panics with ErrNoImpl if nothing128// has been accepted yet. Realms call this on every request, so the panic is129// 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.impl135}136137// TryImpl returns the live implementation and whether there is one, for138// 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, false142 }143 return p.live.impl, true144}145146// Live returns the live release.147func (p *Proxy) Live() (Release, bool) {148 if p.live == nil {149 return Release{}, false150 }151 return *p.live, true152}153154// LivePath returns the realm path of the live implementation, or "" if there155// is none. A state realm that wants to grant write access to whichever156// 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.pkgPath162}163164// 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 out171}172173// 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 out181}182183// Frozen reports whether upgradeability has ended.184func (p *Proxy) Frozen() bool { return p.frozen }185186// Authority returns the current authority.187func (p *Proxy) Authority() Authority { return p.auth }188189// Extensions returns the realm paths allowed to act as the realm holding this190// proxy, in sorted order.191//192// A realm path is fixed, so the exported functions of the realm holding this193// proxy can never grow. What can grow is the set of OTHER realms permitted to194// run against its state: a realm deployed later declares an interface of its195// own, asserts the live implementation to it, and calls through. That is how196// an application adds an entry point it did not ship with. See the197// "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 dup205}206207// 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 own209// path.210func (p *Proxy) IsExtension(pkgPath string) bool {211 if pkgPath == "" {212 return false // a user call is not a realm213 }214 for _, e := range p.exts {215 if e == pkgPath {216 return true217 }218 }219 return false220}221222// 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 can225// Accept an arbitrary implementation can already run arbitrary code against226// the state; naming a second realm that may do the same widens what is227// reachable, not who decides. It does widen the surface a reviewer has to228// 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 return239 }240 at := len(p.exts)241 for i, e := range p.exts {242 if pkgPath < e {243 at = i244 break245 }246 }247 p.exts = append(p.exts, "")248 copy(p.exts[at+1:], p.exts[at:])249 p.exts[at] = pkgPath250 chain.Emit(ExtendEvent, "realm", pkgPath)251}252253// DropExtension revokes pkgPath. The extension realm stays deployed and keeps254// answering calls; it just stops being able to reach the state, so whatever it255// 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 return266 }267 }268 panic(ErrUnknownPath)269}270271// AssertAuthorized panics unless rlm's caller is the authority. It exists for272// wrappers that do work of their own before delegating -- a schema check273// before Accept, say -- so that an unauthorized caller is refused for the274// right reason rather than tripping over the wrapper's own validation first.275func (p *Proxy) AssertAuthorized(_ int, rlm realm) { p.assertAuthorized(0, rlm) }276277// Propose registers impl as a candidate, filed under the path of the realm278// that is calling.279//280// That path comes off the crossing frame rather than from an argument, and281// that is the point of the whole mechanism: a candidate cannot claim to have282// been authored by a path it does not occupy. Whoever accepts it is283// therefore accepting code they can go and read at that path, and a state284// realm that gates writes on LivePath is gating on the same authenticated285// string.286//287// The implementation realm calls this from its own init, so deploying the288// new version is what nominates it. Nothing is served until the authority289// accepts. Re-registering from the same realm replaces that candidate, which290// 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 }299300 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 does304 // have a path -- `gnokey maketx run` executes in /e/<addr>/run --305 // but it does not outlive the transaction, so filing a candidate306 // 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 }313314 p.setPending(&Release{315 pkgPath: from,316 impl: impl,317 height: runtime.ChainHeight(),318 })319 chain.Emit(ProposeEvent, "impl", from)320}321322// Withdraw drops a candidate. The authority may drop any; a realm may always323// drop its own, which is how a superseded candidate stops costing storage324// 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}338339// Accept makes the candidate at pkgPath the live implementation, and files340// the one it replaces in History.341//342// This is the upgrade. It is a separate transaction from the deployment that343// registered the candidate, by a separate principal, which is what gives the344// authority something reviewable to act on -- the same two-phase shape345// gno.land itself uses when a chain parks a submission until an approver346// enables it.347func (p *Proxy) Accept(_ int, rlm realm, pkgPath string) {348 p.assertMutable()349 p.assertAuthorized(0, rlm)350351 i := p.findPending(pkgPath)352 if i < 0 {353 panic(ErrUnknownPath)354 }355 rel := p.pending[i]356 p.removePending(pkgPath)357358 from := ""359 if p.live != nil {360 from = p.live.pkgPath361 p.past = append(p.past, p.live)362 }363 p.live = rel364 chain.Emit(AcceptEvent, "impl", pkgPath, "replaced", from)365}366367// Rollback puts the previous release back in front, and returns the one it368// displaces to the pending set so a fixed version can be re-accepted without369// redeploying it.370//371// This is why History is kept: the release objects stay reachable, and372// reachable is the only form of "still deployed" that matters here. It costs373// 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)377378 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]384385 from := ""386 if p.live != nil {387 from = p.live.pkgPath388 p.setPending(p.live)389 }390 p.live = prev391 chain.Emit(RollbackEvent, "impl", prev.pkgPath, "replaced", from)392}393394// Forget drops the history, releasing the old implementations. Rollback395// stops working -- there is nothing to roll back to -- so this is the "the396// 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 = nil402 chain.Emit(ForgetEvent, "dropped", itoa(dropped))403}404405// 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 state407// 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 = auth416 chain.Emit(AuthorityEvent, "from", old, "to", auth.String())417}418419// Freeze ends upgradeability for good. The live implementation keeps serving420// and nothing can replace it: no unfreeze, by design, because a proxy that421// can be thawed has not actually given anything up. This is how a realm422// graduates from "we are still fixing it" to something other realms can423// build on -- an interrealm contract is only as trustworthy as the least424// mutable realm behind it.425//426// It refuses to freeze with nothing live, which would leave the realm427// permanently unable to answer a call.428//429// Freezing finalizes the extension set. Whatever is registered survives (the430// frozen implementation may depend on it), but afterwards nothing can be added431// and -- because Drop is a mutation too -- nothing can be dropped. So a432// registered extension keeps its access to the realm's state for good. Review433// 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 = true441 p.pending = nil442 p.past = nil443 // Extensions are deliberately kept. They are reachable code that the444 // frozen implementation may depend on, and dropping them here would445 // break entry points that were working a block ago -- a freeze should446 // fix the realm's behavior, not change it.447 chain.Emit(FreezeEvent, "impl", p.live.pkgPath)448}449450// isNested reports whether child is parent, or sits under it. The trailing451// separators are what keep gno.land/r/you/appliance from counting as nested452// under gno.land/r/you/app.453func isNested(parent, child string) bool {454 return strings.HasPrefix(child+"/", parent+"/")455}456457// 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 i462 }463 }464 return -1465}466467// 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 the469// same way on every node.470func (p *Proxy) setPending(rel *Release) {471 if i := p.findPending(rel.pkgPath); i >= 0 {472 p.pending[i] = rel473 return474 }475 at := len(p.pending)476 for i, r := range p.pending {477 if rel.pkgPath < r.pkgPath {478 at = i479 break480 }481 }482 p.pending = append(p.pending, nil)483 copy(p.pending[at+1:], p.pending[at:])484 p.pending[at] = rel485}486487// removePending drops the candidate at pkgPath, reporting whether one was488// there.489func (p *Proxy) removePending(pkgPath string) bool {490 i := p.findPending(pkgPath)491 if i < 0 {492 return false493 }494 p.pending = append(p.pending[:i], p.pending[i+1:]...)495 return true496}497498func (p *Proxy) assertMutable() {499 if p.frozen {500 panic(ErrFrozen)501 }502}503504func (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}513514// 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 < 0520 if neg {521 n = -n522 }523 var buf [20]byte524 i := len(buf)525 for n > 0 {526 i--527 buf[i] = byte('0' + n%10)528 n /= 10529 }530 if neg {531 i--532 buf[i] = '-'533 }534 return string(buf[i:])535}536Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.