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

facade.gno

6.14 Kb · 189 lines
  1// untrusted-render: every path Render echoes was read off a crossing frame in
  2// Propose (cur.Previous().PkgPath(), which a caller cannot forge or type), and
  3// Accept can only promote a key that is already in that tree.
  4//
  5// Package facade is the permanent entry point of the "propose and accept"
  6// upgrade pattern (pattern F of the exploration; see ../../README.md).
  7//
  8// Pattern E lets a deploy take the realm over on the spot. This one splits that
  9// into two steps that different people can hold: an implementation realm
 10// NOMINATES itself from its own init, and the owner ACCEPTS a package path in a
 11// separate transaction. Nothing serves until both have happened.
 12//
 13// The split exists because of a mechanical limit, not just a governance
 14// preference. An implementation is an interface value, and a wallet cannot put
 15// one in a `maketx call` argument: only strings and numbers travel. The
 16// original shape of this pattern (the owner hands the facade an object) is
 17// therefore reachable only from `maketx run` or from another realm. Proposing
 18// from init and accepting by PATH makes both halves ordinary transactions.
 19package facade
 20
 21import (
 22	"strings"
 23
 24	"gno.land/p/nt/avl/v0"
 25	"gno.land/p/nt/ownable/v0"
 26	"gno.land/p/nt/ufmt/v0"
 27)
 28
 29const owner address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul
 30
 31// prefix bounds who may even nominate itself. Accepting is still a separate,
 32// owner-gated decision.
 33const prefix = "gno.land/r/moul/x/upgrade/adminreg/impl/"
 34
 35// Impl is the contract an implementation realm must satisfy.
 36type Impl interface {
 37	Greet(name string) string
 38	Version() string
 39}
 40
 41// The stage ladder, copied in shape from Sui's UpgradeCap: compatible, additive,
 42// dependency-only, immutable, where a policy can only ever become MORE
 43// restrictive and make_immutable discards the cap. CosmWasm (a contract with no
 44// admin) and Solana (an upgrade authority set to None) reach the same place with
 45// one bit; the ladder is better because the interesting states are between
 46// "anything may take this over" and "nothing may ever change again".
 47//
 48// This pattern has an owner and a candidate list, so it has a middle rung the
 49// owner-less selfreg cannot express: no new code, but still free to roll back
 50// among what is already deployed.
 51//
 52// What the top rung does NOT do on its own: freezing this realm ends changes to
 53// the POINTER, not to the code behind it. A private implementation realm can be
 54// re-added at its own path, which would swap behaviour under a frozen facade.
 55// It holds here only because every implementation is public by construction:
 56// handing the facade its own object is exactly what forbids private (see
 57// ../../README.md).
 58const (
 59	StageOpen   = 0 // anything under the prefix may propose, the owner may accept any candidate
 60	StageClosed = 1 // no new candidates; the owner may still accept among those already proposed
 61	StageFrozen = 2 // nothing may be accepted again, whatever is live is final
 62)
 63
 64var (
 65	Ownable = ownable.NewWithAddress(owner)
 66
 67	stage = StageOpen
 68
 69	candidates = avl.NewTree() // pkgpath -> Impl
 70	live       Impl
 71	livePath   string
 72)
 73
 74// Propose nominates the calling realm. Called from the implementation's init,
 75// so deploying makes a candidate and nothing more.
 76func Propose(cur realm, impl Impl) {
 77	if stage != StageOpen {
 78		panic("adminreg/facade/v0 is " + StageName() + ", no new candidate may be proposed")
 79	}
 80	caller := cur.Previous().PkgPath()
 81	if !strings.HasPrefix(caller, prefix) {
 82		panic("unauthorized: " + caller + " is not under " + prefix)
 83	}
 84	if impl == nil {
 85		panic("implementation must not be nil")
 86	}
 87	candidates.Set(caller, impl)
 88}
 89
 90// Accept promotes a proposed path to live. Owner-gated, and it takes a STRING,
 91// so it is callable straight from a wallet.
 92func Accept(cur realm, pkgPath string) {
 93	Ownable.AssertOwnedBy(cur.Previous().Address())
 94	if stage == StageFrozen {
 95		panic("adminreg/facade/v0 is frozen, " + livePath + " is final")
 96	}
 97	v := candidates.Get(pkgPath)
 98	if v == nil {
 99		panic("no candidate at " + pkgPath)
100	}
101	live, livePath = v.(Impl), pkgPath
102}
103
104// Close stops new candidates. The owner may still accept among those already
105// proposed, so a rollback stays possible while new code does not.
106func Close(cur realm) {
107	tighten(cur, StageClosed)
108}
109
110// Freeze ends this realm's upgradeability, forever. There is no rung above it
111// and nothing takes it back: that is the whole point, and it is the only way out
112// of every caller trusting the owner rather than the code.
113func Freeze(cur realm) {
114	tighten(cur, StageFrozen)
115}
116
117// tighten is the ratchet. Owner-gated, and it refuses to loosen: the stage is
118// the one piece of state here that a later owner cannot undo.
119func tighten(cur realm, to int) {
120	Ownable.AssertOwnedBy(cur.Previous().Address())
121	if to <= stage {
122		panic("the stage ladder only tightens, and this realm is already " + StageName())
123	}
124	stage = to
125}
126
127// Stage is the rung this realm is on. It only ever goes up.
128func Stage() int {
129	return stage
130}
131
132// StageName is Stage as the word a caller reads in Render.
133func StageName() string {
134	switch stage {
135	case StageFrozen:
136		return "frozen"
137	case StageClosed:
138		return "closed"
139	default:
140		return "open"
141	}
142}
143
144// Live is the package path currently serving, or "" before the first Accept.
145func Live() string {
146	return livePath
147}
148
149// Candidates lists every path that has nominated itself, in order.
150func Candidates() []string {
151	out := []string{}
152	candidates.Iterate("", "", func(k string, _ any) bool {
153		out = append(out, k)
154		return false
155	})
156	return out
157}
158
159// Greet forwards to the accepted implementation.
160func Greet(name string) string {
161	assertLive()
162	return live.Greet(name)
163}
164
165// Version reports the accepted implementation's own version string.
166func Version() string {
167	assertLive()
168	return live.Version()
169}
170
171func assertLive() {
172	if live == nil {
173		panic("no implementation accepted")
174	}
175}
176
177func Render(_ string) string {
178	out := ufmt.Sprintf("adminreg/facade/v0 [%s]\n", StageName())
179	if live == nil {
180		out += "live: none accepted\n"
181	} else {
182		out += ufmt.Sprintf("live: %s (%s)\n%s\n", live.Version(), livePath, live.Greet("world"))
183	}
184	out += ufmt.Sprintf("candidates: %d\n", candidates.Size())
185	for _, p := range Candidates() {
186		out += "- " + p + "\n"
187	}
188	return out
189}