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

verifier.gno

10.85 Kb · 253 lines
  1// Package names enforces namespace permissions for package deployment.
  2//
  3// Two namespace shapes grant deploy authority when enforcement is enabled:
  4//
  5//  1. PA (personal-address) namespaces — gno.land/{r,p}/<addr>/* — the
  6//     deployer's address string equals the namespace literal. Anyone can
  7//     deploy under their own address.
  8//
  9//  2. Registered-name namespaces — gno.land/{r,p}/<name>/* — r/sys/users
 10//     has a (name → addr) mapping where the resolved address equals the
 11//     deployer AND the name is the user's CURRENT name (not a historical
 12//     alias from a rename chain). This is the bridge that lets
 13//     r/sys/namereg/v0 (or any other DAO-whitelisted controller) grant
 14//     deploy authority via name registration.
 15//
 16// Authority is unscoped: a registered name owns BOTH r/<name>/* and
 17// p/<name>/* paths. There is no sub-prefix isolation (e.g. r/u/<name>/*).
 18//
 19// The realm exposes an emergency-halt switch via SetPaused. When paused,
 20// the verifier rejects EVERY namespace check — PA included — until
 21// unpaused. This is the "true emergency" semantic; the narrow alternative
 22// (pause registered-name only, preserve PA) was considered and rejected
 23// because the threats most likely to justify pausing this realm
 24// (verifier bug, compromised controller, signature-layer incident) do
 25// not reliably exempt PA from the same blast radius.
 26package names
 27
 28import (
 29	"chain"
 30
 31	"gno.land/r/gov/dao"
 32	govimpl "gno.land/r/gov/dao/impl/v0"
 33	memberstore "gno.land/r/gov/dao/memberstore/v0"
 34	susers "gno.land/r/sys/users"
 35)
 36
 37var (
 38	// admin is the GovDAO T1 multisig address, hardcoded at realm-source
 39	// commit time. Its only capability is gating Enable() — a one-way,
 40	// one-shot genesis activation of the namespace verifier. The address
 41	// has no other authority on this realm; pause/unpause is gated on a
 42	// separate GovDAO T1 proposal (see ProposeSetPaused), and there is
 43	// no SetEnabled(false) or SetAdmin path.
 44	//
 45	// Hardcoding is acceptable because:
 46	//   - Enable() is called once, at chain genesis. After that the
 47	//     address is dead weight — no further capability flows through it.
 48	//   - The narrow blast radius of "stale admin" is "Enable() can never
 49	//     be called", which leaves the verifier in pre-Enable bypass mode
 50	//     (returns true for all checks) — degraded but not exploitable.
 51	//   - A rotation path would only matter if Enable() needed to be
 52	//     re-issued. It doesn't; the flag is sticky.
 53	//
 54	// If the genesis activation pattern ever needs to change (e.g. a
 55	// SetEnabled(false) emergency disable is added later), this admin
 56	// model should be replaced with a GovDAO T1 proposal flow that
 57	// mirrors ProposeSetPaused. Until then, the hardcoded address is
 58	// the smallest viable governance surface for the one-shot use case.
 59	admin   = address("g1skl80cuz8zq3lul9pgz5pc35l2pfzgxgfpsqkx")
 60	enabled = false
 61	paused  = false
 62)
 63
 64// nameLookupFn returns (addr, ok) for a given registered name. Allows
 65// the verifier function to be unit-tested without wiring up r/sys/users
 66// state — production binds resolveCurrentName as the lookup, tests pass
 67// nil (PA-only) or a fake.
 68type nameLookupFn func(name string) (addr address, ok bool)
 69
 70// IsAuthorizedAddressForNamespace checks if the given address can deploy
 71// to the given namespace. See package doc for the two authorization paths
 72// and the pause semantic.
 73//
 74// Pre-Enable, all checks pass (testing/dev convenience).
 75func IsAuthorizedAddressForNamespace(address_XXX address, namespace string) bool {
 76	return verifier(enabled, paused, address_XXX, namespace, resolveCurrentName)
 77}
 78
 79// resolveCurrentName is the production nameLookupFn, backed by r/sys/users.
 80// Returns ok=true only if the name resolves to a non-deleted user AND the
 81// queried name is that user's CURRENT name (the most recent UpdateName).
 82//
 83// Restricting to the current name has two consequences worth knowing:
 84//
 85//  1. After a UpdateName from "alice" to "alice2", the user keeps deploy
 86//     authority over r/alice2/* but LOSES it for r/alice/*. Already-
 87//     deployed packages at r/alice/* keep working — deploy-time
 88//     authorization doesn't unwind the past — but no NEW deploys can
 89//     land there.
 90//
 91//  2. The old name "alice" is also unregisterable by anyone else: when
 92//     r/sys/users.UpdateName runs, it inserts the new name into nameStore
 93//     but does not remove the old one. r/sys/users.RegisterUser then
 94//     rejects re-registration of "alice" with ErrNameTaken. Net effect:
 95//     a rename permanently removes the old name from circulation.
 96//
 97// The alternative (allow historical aliases to retain authority) was
 98// rejected because it lets a single user register one cheap name, then
 99// rename N times to claim authority over N distinct namespaces — a
100// stealth namespace acquisition vector worse than the current
101// burn-on-rename behavior.
102func resolveCurrentName(name string) (address, bool) {
103	data, isCurrent := susers.ResolveName(name)
104	if data == nil || !isCurrent {
105		return "", false
106	}
107	return data.Addr(), true
108}
109
110// Enable enables the namespace check for this realm.
111// The namespace check is disabled initially to ease txtar and other testing contexts,
112// but this function is meant to be called in the genesis of a chain.
113func Enable(cur realm) {
114	if !cur.IsCurrent() {
115		panic("unauthorized: cur is not the caller's live realm")
116	}
117	if cur.Previous().Address() != admin {
118		panic("caller is not admin")
119	}
120	enabled = true
121}
122
123func IsEnabled() bool {
124	return enabled
125}
126
127// ProposeSetPaused returns a GovDAO proposal request that, when voted
128// through and executed, toggles the chain-wide deploy gate. When the
129// realm is paused, the verifier rejects EVERY namespace check — PA
130// (personal-address) included — until a subsequent ProposeSetPaused(false)
131// proposal executes.
132//
133// This is an emergency halt. A paused state means NO new MsgAddPackage
134// transactions land at any path on the chain. Existing realms continue
135// to receive MsgCall traffic normally — pause is scoped to addpkg, not
136// to all VM operations. Use cases:
137//   - Bug discovered in this realm or r/sys/users that requires a
138//     hotfix before further deploys can be trusted.
139//   - Wallet/signature-layer incident under investigation.
140//
141// (A "compromised controller" use case was considered and removed: the
142// controller's RegisterUser path is direct into r/sys/users and does
143// NOT go through this verifier, so pause does not freeze new
144// registrations. To contain a compromised controller, the appropriate
145// flow is ProposeControllerRemoval in r/sys/users, not pause here.)
146//
147// The narrow alternative (pause registered-name path only, preserve
148// PA) was considered and rejected. See package doc for rationale.
149//
150// Gated on GovDAO proposal at T1 tier — not the hardcoded admin used
151// by Enable. Pause is consequential enough to warrant a tier-restricted
152// governance vote rather than a single-multisig click. T1 filter
153// prevents lower-tier members from spamming pause proposals to dilute
154// attention. Trade-off is response time: a T1 vote takes hours-to-days;
155// if a faster emergency-halt mechanism is needed, that belongs at a
156// different layer (e.g. an ante-handler-level chain pause), not here.
157//
158// Pause is orthogonal to the pre-Enable bypass: before Enable, the
159// verifier returns true regardless of paused state. So executing a
160// pause proposal before Enable has no effect on deploys, but the
161// value persists and applies the moment Enable runs. To avoid this
162// staging trap, operators should call Enable BEFORE any pause
163// proposals are voted in.
164//
165// Idempotency: calling ProposeSetPaused(v) when the realm's current
166// paused state already equals v panics at proposal-creation time so
167// voters never see a proposal whose execution would no-op.
168func ProposeSetPaused(cur realm, v bool) dao.ProposalRequest {
169	if paused == v {
170		panic("paused state already matches requested value; no-op proposal rejected")
171	}
172	cb := func(cur realm) error {
173		setPaused(0, cur, v)
174		return nil
175	}
176	title := "Unpause Namespace Verifier"
177	desc := "This proposal unpauses `r/sys/names`. After execution, the namespace verifier will resume normal authorization checks (PA + registered-name paths). MsgCall traffic to existing realms is unaffected (pause was scoped to MsgAddPackage); only NEW package deploys gate on the unpaused state."
178	if v {
179		title = "Pause Namespace Verifier"
180		desc = "This proposal pauses `r/sys/names`. After execution, the namespace verifier will reject EVERY new MsgAddPackage on the chain — PA (personal-address) and registered-name namespaces alike — until a subsequent unpause proposal executes. This is an emergency halt scoped to addpkg; MsgCall traffic to existing realms is unaffected."
181	}
182	return dao.NewProposalRequestWithFilter(
183		title,
184		desc,
185		dao.NewSimpleExecutor(0, cur, cb, ""),
186		govimpl.NewFilterByTier(memberstore.T1),
187	)
188}
189
190// setPaused is the private actuator behind ProposeSetPaused's executor
191// callback. It is unexported to ensure no path outside the proposal
192// flow can flip the flag — every state change goes through GovDAO.
193//
194// Emits NamespaceEnforcement{Paused,Unpaused} with the executor's
195// realm path for off-chain audit trails.
196func setPaused(_ int, rlm realm, v bool) {
197	paused = v
198	if v {
199		chain.Emit("NamespaceEnforcementPaused", "by", rlm.Previous().PkgPath())
200	} else {
201		chain.Emit("NamespaceEnforcementUnpaused", "by", rlm.Previous().PkgPath())
202	}
203}
204
205// IsPaused reports the current value of the pause flag. Note: when
206// the realm is pre-Enable, IsPaused may return true but the verifier
207// will still pass-through (pre-Enable bypass takes priority).
208func IsPaused() bool {
209	return paused
210}
211
212// verifier checks namespace deployment permissions.
213// lookup is the registered-name resolver — pass nil to disable that path
214// (used by tests that want to exercise PA-only behavior).
215//
216// Order of checks (top to bottom, first matching wins):
217//  1. !isEnabled → return true   (pre-Enable: testing/dev bypass)
218//  2. isPaused   → return false  (emergency halt — INCLUDES PA)
219//  3. invalid input → return false
220//  4. PA match (addr.String() == namespace) → return true
221//  5. Registered-name lookup match → return true
222//  6. otherwise → return false
223//
224// The pause check is intentionally above the PA check. A SetPaused(true)
225// halts every deploy regardless of namespace shape.
226func verifier(isEnabled, isPaused bool, address_XXX address, namespace string, lookup nameLookupFn) bool {
227	if !isEnabled {
228		return true // pre-genesis / dev convenience: bypass everything
229	}
230
231	if isPaused {
232		return false // emergency halt: reject every deploy including PA
233	}
234
235	if namespace == "" || !address_XXX.IsValid() {
236		return false
237	}
238
239	// Path 1: PA (personal-address) namespace.
240	// gno.land/{p,r}/{ADDRESS}/**
241	if address_XXX.String() == namespace {
242		return true
243	}
244
245	// Path 2: registered-name namespace via r/sys/users.
246	if lookup != nil {
247		if owner, ok := lookup(namespace); ok && owner == address_XXX {
248			return true
249		}
250	}
251
252	return false
253}