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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

proxy.gno

6.92 Kb · 206 lines
  1package config
  2
  3import (
  4	"strings"
  5
  6	"chain"
  7
  8	"gno.land/p/nt/avl/v0"
  9)
 10
 11// Version chaining: how v2 and v3 get built without splitting the state.
 12//
 13// # The shape, and why it is this way round
 14//
 15// This realm is public, so its path is permanent and a new API means a new
 16// version at a new path. Left alone, that fragments everything: a realm
 17// importing v1 and a realm importing v2 would read two different member lists
 18// and two different pause switches, and the second one to be written would win
 19// for half the callers.
 20//
 21// Delegation fixes it, and it can only run one way. A version can import the
 22// versions that already existed when it was written; it can never import one
 23// that does not exist yet. So the STATE STAYS IN THE OLDEST version that has
 24// it, and every later version is a thin relay in front:
 25//
 26//	v3  ->  v2  ->  v1        (this one: the root, and where everything lives)
 27//
 28// A realm importing v1, v2 or v3 is then reading the same settings, the same
 29// pause and the same managers, whichever door it came through. That is the
 30// property worth all of this.
 31//
 32// v0 cannot take part: it is already on chain and has no settings store at
 33// all. It keeps answering for its own member list and nothing else.
 34//
 35// # Writing v2
 36//
 37// v2 imports this package, holds no state of its own, and for each write it
 38// exposes, calls SetAs or UnsetAs with the address it was called by:
 39//
 40//	func Set(cur realm, key, value string) {
 41//		config.SetAs(cross(cur), cur.Previous().Address(), key, value)
 42//	}
 43//
 44// Reads need none of this. They are plain borrowed calls, so v2 just forwards:
 45//
 46//	func Get(key string) string { return config.Get(key) }
 47//
 48// Then, once, from a manager's address:
 49//
 50//	AllowProxy gno.land/r/moul/config/v2
 51//
 52// # What a proxy is trusted with, exactly
 53//
 54// A registered proxy is trusted to say WHO is asking, not to decide whether
 55// they may. SetAs still puts the principal through the Authorizer, so the
 56// member list stays the single answer to "who may change config" for every
 57// version at once, and adding a manager here works through v2 and v3 with no
 58// further deploys.
 59//
 60// It is not a security boundary against the proxy's own code: a proxy that
 61// lies about its principal writes whatever it likes. It does not have to be,
 62// because the same person deploys both and registering one is a deliberate,
 63// revocable act. What it buys is that v2 never carries a copy of the member
 64// list, so the two can never disagree.
 65
 66// proxies holds the package paths allowed to relay, keyed by path, valued by
 67// the address that path resolves to. The address is what an incoming call is
 68// actually compared against; the path is stored so Render and ListProxies can
 69// show something a human recognises.
 70var proxies = avl.NewTree() // pkgPath -> address
 71
 72// AllowProxy registers a newer version of this realm as a relay. Manager only.
 73//
 74// It takes a package path rather than an address because a path is what a
 75// manager can check by eye; the address is derived here with the same function
 76// the chain uses, so the two cannot drift.
 77func AllowProxy(cur realm, pkgPath string) {
 78	assertProxyPath(pkgPath)
 79	assertAuthorized(cur, "config.AllowProxy:"+pkgPath)
 80
 81	proxies.Set(pkgPath, chain.PackageAddress(pkgPath))
 82	settingsRev++
 83	chain.Emit("ConfigAllowProxy", "path", pkgPath,
 84		"address", chain.PackageAddress(pkgPath).String())
 85}
 86
 87// RevokeProxy withdraws a relay. Manager only.
 88//
 89// Revoking one that was never registered aborts rather than passing quietly:
 90// at this point in an incident, "done" and "you misspelled it" must not look
 91// the same.
 92func RevokeProxy(cur realm, pkgPath string) {
 93	assertProxyPath(pkgPath)
 94	assertAuthorized(cur, "config.RevokeProxy:"+pkgPath)
 95
 96	if _, removed := proxies.Remove(pkgPath); !removed {
 97		panic("no such proxy: " + pkgPath)
 98	}
 99	settingsRev++
100	chain.Emit("ConfigRevokeProxy", "path", pkgPath)
101}
102
103// IsProxy reports whether addr is a registered relay.
104func IsProxy(addr address) bool {
105	if addr == "" {
106		return false
107	}
108	found := false
109	proxies.Iterate("", "", func(_ string, value any) bool {
110		if value.(address) == addr {
111			found = true
112			return true // stop
113		}
114		return false
115	})
116	return found
117}
118
119// ListProxies returns the registered relay paths, in sorted order.
120func ListProxies() []string {
121	out := []string{}
122	proxies.Iterate("", "", func(key string, _ any) bool {
123		out = append(out, key)
124		return false
125	})
126	return out
127}
128
129// SetAs is Set, performed by a registered proxy on behalf of principal.
130//
131// Two checks, and both have to hold: the caller is a registered relay, and the
132// principal it names is authorized. Neither is redundant. Dropping the first
133// would let any realm claim any principal; dropping the second would make a
134// registered proxy an unconditional bypass of the member list.
135func SetAs(cur realm, principal address, key, value string) {
136	assertRelay(cur)
137	assertWritableKey(key)
138	assertValue(key, value)
139	assertPrincipal(principal, "config.Set:"+key)
140	writeSetting(key, value)
141}
142
143// UnsetAs is Unset, performed by a registered proxy on behalf of principal.
144func UnsetAs(cur realm, principal address, key string) {
145	assertRelay(cur)
146	assertWritableKey(key)
147	assertPrincipal(principal, "config.Unset:"+key)
148	removeSetting(key)
149}
150
151// assertRelay aborts unless the immediate caller is a registered proxy.
152func assertRelay(cur realm) {
153	from := cur.Previous().Address()
154	if !IsProxy(from) {
155		panic("not a registered proxy: " + from.String())
156	}
157}
158
159// assertPrincipal puts an address through the Authorizer without it being the
160// caller, which is what a relay needs and what DoByPrevious cannot express.
161//
162// With the default MemberAuthority this is a membership test. With an
163// authority that inspects the caller instead (a ContractAuthority), the
164// relayed path asks that authority about the principal, so check what it does
165// before transferring management while a proxy is registered.
166func assertPrincipal(principal address, title string) {
167	if principal == "" {
168		panic("unauthorized: no principal")
169	}
170	allowed := false
171	err := Authorizer.Authority().Authorize(principal, title, func() error {
172		allowed = true
173		return nil
174	})
175	if err != nil {
176		panic("unauthorized: " + err.Error())
177	}
178	if !allowed {
179		panic("unauthorized")
180	}
181}
182
183// assertProxyPath refuses anything that is not a realm path under this
184// namespace. A proxy is by definition a later version of this realm, so
185// nothing else should ever be registered, and the narrow check turns a
186// fat-fingered AllowProxy into an abort rather than a standing grant to an
187// unrelated realm.
188func assertProxyPath(pkgPath string) {
189	const want = "gno.land/r/moul/config/v"
190	if !strings.HasPrefix(pkgPath, want) {
191		panic("a proxy must be a later version of this realm, got " + pkgPath)
192	}
193	rest := strings.TrimPrefix(pkgPath, want)
194	if rest == "" || !isDigits(rest) {
195		panic("a proxy must be a later version of this realm, got " + pkgPath)
196	}
197}
198
199func isDigits(s string) bool {
200	for i := 0; i < len(s); i++ {
201		if s[i] < '0' || s[i] > '9' {
202			return false
203		}
204	}
205	return len(s) > 0
206}