PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/upgradeable/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm / upgradeable
Files
4 (gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/upgradeable/v0
gno
0.9

Files (4)

  • gnomod.tomltoml
  • authority.gnogno
  • errors.gnogno
  • upgradeable.gnogno
upgradeable.gnogno
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}536

Functions

not supported for pure packages by the node (vm/qfuncs)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.