Search Apps Documentation Source Content File Folder Download Copy Actions Download

verifier.gno

10.77 Kb · 250 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/v1 (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/v3/impl"
 33	memberstore "gno.land/r/gov/dao/v3/memberstore"
 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("g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh")
 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.Previous().Address() != admin {
115		panic("caller is not admin")
116	}
117	enabled = true
118}
119
120func IsEnabled() bool {
121	return enabled
122}
123
124// ProposeSetPaused returns a GovDAO proposal request that, when voted
125// through and executed, toggles the chain-wide deploy gate. When the
126// realm is paused, the verifier rejects EVERY namespace check — PA
127// (personal-address) included — until a subsequent ProposeSetPaused(false)
128// proposal executes.
129//
130// This is an emergency halt. A paused state means NO new MsgAddPackage
131// transactions land at any path on the chain. Existing realms continue
132// to receive MsgCall traffic normally — pause is scoped to addpkg, not
133// to all VM operations. Use cases:
134//   - Bug discovered in this realm or r/sys/users that requires a
135//     hotfix before further deploys can be trusted.
136//   - Wallet/signature-layer incident under investigation.
137//
138// (A "compromised controller" use case was considered and removed: the
139// controller's RegisterUser path is direct into r/sys/users and does
140// NOT go through this verifier, so pause does not freeze new
141// registrations. To contain a compromised controller, the appropriate
142// flow is ProposeControllerRemoval in r/sys/users, not pause here.)
143//
144// The narrow alternative (pause registered-name path only, preserve
145// PA) was considered and rejected. See package doc for rationale.
146//
147// Gated on GovDAO proposal at T1 tier — not the hardcoded admin used
148// by Enable. Pause is consequential enough to warrant a tier-restricted
149// governance vote rather than a single-multisig click. T1 filter
150// prevents lower-tier members from spamming pause proposals to dilute
151// attention. Trade-off is response time: a T1 vote takes hours-to-days;
152// if a faster emergency-halt mechanism is needed, that belongs at a
153// different layer (e.g. an ante-handler-level chain pause), not here.
154//
155// Pause is orthogonal to the pre-Enable bypass: before Enable, the
156// verifier returns true regardless of paused state. So executing a
157// pause proposal before Enable has no effect on deploys, but the
158// value persists and applies the moment Enable runs. To avoid this
159// staging trap, operators should call Enable BEFORE any pause
160// proposals are voted in.
161//
162// Idempotency: calling ProposeSetPaused(v) when the realm's current
163// paused state already equals v panics at proposal-creation time so
164// voters never see a proposal whose execution would no-op.
165func ProposeSetPaused(cur realm, v bool) dao.ProposalRequest {
166	if paused == v {
167		panic("paused state already matches requested value; no-op proposal rejected")
168	}
169	cb := func(cur realm) error {
170		setPaused(0, cur, v)
171		return nil
172	}
173	title := "Unpause Namespace Verifier"
174	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."
175	if v {
176		title = "Pause Namespace Verifier"
177		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."
178	}
179	return dao.NewProposalRequestWithFilter(
180		title,
181		desc,
182		dao.NewSimpleExecutor(0, cur, cb, ""),
183		govimpl.NewFilterByTier(memberstore.T1),
184	)
185}
186
187// setPaused is the private actuator behind ProposeSetPaused's executor
188// callback. It is unexported to ensure no path outside the proposal
189// flow can flip the flag — every state change goes through GovDAO.
190//
191// Emits NamespaceEnforcement{Paused,Unpaused} with the executor's
192// realm path for off-chain audit trails.
193func setPaused(_ int, rlm realm, v bool) {
194	paused = v
195	if v {
196		chain.Emit("NamespaceEnforcementPaused", "by", rlm.Previous().PkgPath())
197	} else {
198		chain.Emit("NamespaceEnforcementUnpaused", "by", rlm.Previous().PkgPath())
199	}
200}
201
202// IsPaused reports the current value of the pause flag. Note: when
203// the realm is pre-Enable, IsPaused may return true but the verifier
204// will still pass-through (pre-Enable bypass takes priority).
205func IsPaused() bool {
206	return paused
207}
208
209// verifier checks namespace deployment permissions.
210// lookup is the registered-name resolver — pass nil to disable that path
211// (used by tests that want to exercise PA-only behavior).
212//
213// Order of checks (top to bottom, first matching wins):
214//  1. !isEnabled → return true   (pre-Enable: testing/dev bypass)
215//  2. isPaused   → return false  (emergency halt — INCLUDES PA)
216//  3. invalid input → return false
217//  4. PA match (addr.String() == namespace) → return true
218//  5. Registered-name lookup match → return true
219//  6. otherwise → return false
220//
221// The pause check is intentionally above the PA check. A SetPaused(true)
222// halts every deploy regardless of namespace shape.
223func verifier(isEnabled, isPaused bool, address_XXX address, namespace string, lookup nameLookupFn) bool {
224	if !isEnabled {
225		return true // pre-genesis / dev convenience: bypass everything
226	}
227
228	if isPaused {
229		return false // emergency halt: reject every deploy including PA
230	}
231
232	if namespace == "" || !address_XXX.IsValid() {
233		return false
234	}
235
236	// Path 1: PA (personal-address) namespace.
237	// gno.land/{p,r}/{ADDRESS}/**
238	if address_XXX.String() == namespace {
239		return true
240	}
241
242	// Path 2: registered-name namespace via r/sys/users.
243	if lookup != nil {
244		if owner, ok := lookup(namespace); ok && owner == address_XXX {
245			return true
246		}
247	}
248
249	return false
250}