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

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}