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

run_submitters.gno

16.03 Kb · 374 lines
  1package params
  2
  3// Delegated management of vm:p:run_submitters, the allowlist of addresses
  4// permitted to send MsgRun.
  5//
  6// See delegate.gno for why this is a named slot rather than a registry.
  7//
  8// What the delegate can and cannot do, and why the asymmetry is this way round:
  9//
 10//   - It may ADD addresses. This is the routine work the delegation exists for.
 11//   - It may REMOVE only addresses it added itself, and never the last one.
 12//     De-listing its own mistake is a core part of managing an allowlist, so
 13//     add-only would be a strange capability to hand out. Two bounds keep that
 14//     safe. Grant-scoping stops it removing an address that predates the
 15//     delegation. The non-empty floor in RemoveRunSubmitters stops it reaching
 16//     zero by any route — an empty list means the gate is OFF and anyone may
 17//     MsgRun, so emptying it would let the delegate revoke the entire
 18//     restriction GovDAO voted for, which is the one thing this capability must
 19//     not be able to do.
 20//   - GovDAO retains everything, through ProposeSetRunSubmitters below -- the
 21//     generic factories no longer accept this key. That is the bounded reset:
 22//     one proposal returns the key to a known-good list regardless of what the
 23//     delegate did, and the proposal shows voters the exact resulting list.
 24//
 25// A cost to state plainly rather than bury: this key is read by the ante handler
 26// on EVERY transaction, before the per-tx gas meter exists, so its length is an
 27// unmetered per-transaction constant for the whole chain. The delegate therefore
 28// holds a knob on that constant, bounded only by maxAddressListLen in
 29// gno.land/pkg/sdk/vm/params.go. That bound is enforced on this path for free:
 30// UpdateSysParamStrings re-sets the whole list, which re-enters WillSetParam and
 31// Params.Validate, so both the cap and bech32 validation apply to a delegate's
 32// additions. A key that is not read on the ante path would be a cheaper first
 33// delegation; this one is the one that was asked for.
 34
 35import (
 36	"chain"
 37	prms "sys/params"
 38
 39	"gno.land/p/moul/addrset/v0"
 40
 41	"gno.land/r/gov/dao"
 42)
 43
 44const (
 45	vmModulePrefix    = "vm"
 46	vmParamsSubmodule = "p"
 47
 48	runSubmittersKey = "run_submitters"
 49)
 50
 51// runSubmittersMgr is the package path authorized to manage run_submitters.
 52// Empty means the capability is not delegated, and empty must deny — see
 53// assertDelegate.
 54var runSubmittersMgr string
 55
 56// runSubmittersGrants records which addresses the current delegate added, so
 57// removal can be scoped to its own grants.
 58//
 59// The parameter is the source of truth and this is a side table, so the two can
 60// disagree — genesis, `gnogenesis params set`, or any future direct keeper write
 61// produces entries with no grant recorded. That direction is safe: an unrecorded
 62// address is simply not removable by the delegate, which is the conservative
 63// answer. Cleared whenever the delegation changes hands, so a new delegate never
 64// inherits authority over its predecessor's grants.
 65var runSubmittersGrants = addrset.Set{}
 66
 67// RunSubmittersManager returns the package path currently authorized to manage
 68// run_submitters, or "" when the capability is not delegated.
 69func RunSubmittersManager() string {
 70	return runSubmittersMgr
 71}
 72
 73// RunSubmittersGrantedBy reports whether the current delegate added addr, i.e.
 74// whether it may remove it.
 75//
 76// Exposed so a delegate can check before acting. A delegate that discovers a
 77// refusal by panicking mid-proposal-execution is in a bad place: the panic
 78// aborts the transaction, and for a DAO whose proposal has already passed, every
 79// retry aborts the same way.
 80func RunSubmittersGrantedBy(addr address) bool {
 81	return runSubmittersGrants.Has(addr)
 82}
 83
 84// IsRunSubmittersDelegate reports whether pkgpath currently holds the
 85// capability. Pure predicate, for a caller that wants to fail cleanly rather
 86// than be panicked at.
 87func IsRunSubmittersDelegate(pkgpath string) bool {
 88	return pkgpath != "" && pkgpath == runSubmittersMgr
 89}
 90
 91// ProposeSetRunSubmittersManager creates a GovDAO proposal handing management of
 92// run_submitters to pkgpath.
 93//
 94// pkgpath may be a sub-realm identity such as
 95// "gno.land/r/nt/commondao/v0#dao/42", which is how a single DAO hosted by a
 96// multi-tenant realm is named. Matching is exact, so naming the bare host would
 97// authorize the host itself and none of its DAOs.
 98func ProposeSetRunSubmittersManager(cur realm, pkgpath string) dao.ProposalRequest {
 99	assertDelegatePath(pkgpath)
100	if pkgpath == runSubmittersMgr {
101		panic("no-op proposal rejected: " + pkgpath + " already manages " + runSubmittersKey)
102	}
103
104	// desc uses the manager as of proposal creation, which is the honest thing
105	// to show a voter. The EVENT reads it again inside the executor, because two
106	// proposals created while the slot is empty and executed in sequence would
107	// otherwise both report from="" while the second actually replaced the first.
108	from := runSubmittersMgr
109	desc := "Authorize " + pkgpath + " to add addresses to the " + runSubmittersKey +
110		" allowlist, which gates who may send MsgRun. It may remove only " +
111		"addresses it added itself. GovDAO retains full control, including " +
112		"replacing the whole list."
113	if from != "" {
114		desc += " This replaces the current manager, " + from +
115			", and discards the record of which addresses it granted."
116	}
117
118	return newDelegateProposal(cur, DelegateSetEvent, runSubmittersKey, pkgpath,
119		"Delegate "+runSubmittersKey+" management", desc,
120		func() string {
121			prev := runSubmittersMgr
122			runSubmittersMgr = pkgpath
123			// A new holder must not inherit removal authority over addresses
124			// the previous one granted.
125			runSubmittersGrants = addrset.Set{}
126			return prev
127		})
128}
129
130// ProposeClearRunSubmittersManager creates a GovDAO proposal revoking the
131// delegation.
132//
133// Revocation is immediate on execution because the slot is consulted on every
134// call. It deliberately does NOT remove addresses the delegate added: sweeping
135// them would make the executed effect invisible at vote time, and would silently
136// remove nothing whenever the grant record had drifted. Use the existing
137// whole-list setter to reset the list to a reviewed value.
138func ProposeClearRunSubmittersManager(cur realm) dao.ProposalRequest {
139	if runSubmittersMgr == "" {
140		panic("no-op proposal rejected: " + runSubmittersKey + " is not delegated")
141	}
142
143	from := runSubmittersMgr
144	return newDelegateProposal(cur, DelegateClearedEvent, runSubmittersKey, "",
145		"Revoke "+runSubmittersKey+" management",
146		"Revoke "+from+"'s authority to manage the "+runSubmittersKey+
147			" allowlist. Addresses it already added REMAIN on the list; reset the "+
148			"list explicitly if that is not wanted.",
149		func() string {
150			prev := runSubmittersMgr
151			runSubmittersMgr = ""
152			runSubmittersGrants = addrset.Set{}
153			return prev
154		})
155}
156
157// AddRunSubmitters adds addresses to the run_submitters allowlist.
158//
159// Callable only by the delegated manager. Addresses already present are a no-op
160// (UpdateSysParamStrings dedupes), and the chain still validates every entry and
161// enforces the list-length cap, because the update re-sets the whole list.
162func AddRunSubmitters(cur realm, addrs []string) {
163	assertDelegate(0, cur, runSubmittersMgr, "the "+runSubmittersKey+" allowlist")
164	if len(addrs) == 0 {
165		return
166	}
167
168	// The delegate may curate a list that is already in force. It may not put
169	// one into force.
170	//
171	// An empty run_submitters means the allowlist is OFF and anyone may MsgRun.
172	// So the first add is not curation: it switches a chain-wide restriction on
173	// and picks who it admits. A delegate adding one address to an empty list
174	// leaves that address the only one on the chain that may run code.
175	//
176	// Refused rather than discouraged because it is unrepairable. Creating a
177	// GovDAO proposal needs MsgRun -- a ProposalRequest carries an Executor,
178	// which MsgCall cannot build from string arguments -- so once the gate is
179	// armed against the members they cannot propose the vote that would undo
180	// it, and the floor in RemoveRunSubmitters stops the delegate undoing it
181	// either.
182	if len(GetRunSubmitters()) == 0 {
183		panic("refusing to arm the " + runSubmittersKey +
184			" allowlist: it is empty, so the gate is off and anyone may MsgRun. " +
185			"Turning it on is a GovDAO vote, not a delegated edit")
186	}
187
188	// Record a grant only for an address this call actually ADDED.
189	//
190	// UpdateSysParamStrings dedupes on add, so passing an address already on the
191	// list leaves the parameter unchanged. Recording a grant for it anyway would
192	// let the delegate launder authority over entries it never granted: read the
193	// list, re-add all of it (a no-op on the parameter, but every address now
194	// recorded as its own), then remove all of it -- including whatever predated
195	// the delegation. The floor in RemoveRunSubmitters would refuse the last of
196	// those removals, but only the last: everything up to it would still go
197	// through, leaving the delegate holding the only listed address and so the
198	// sole authority over who may MsgRun.
199	//
200	// So the grant record has to follow the parameter, not the argument.
201	present := make(map[string]bool)
202	for _, a := range GetRunSubmitters() {
203		present[a] = true
204	}
205
206	prms.UpdateSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey, addrs, true)
207	for _, a := range addrs {
208		if !present[a] {
209			runSubmittersGrants.Add(address(a))
210		}
211	}
212	chain.Emit(DelegateWriteEvent,
213		"key", runSubmittersKey, "realm", runSubmittersMgr, "op", "add")
214}
215
216// RemoveRunSubmitters removes addresses from the run_submitters allowlist.
217//
218// Callable only by the delegated manager, and only for addresses that manager
219// added. Refusing rather than silently skipping is deliberate: a partial removal
220// that reported success would leave the caller believing an address was
221// de-listed when it was not.
222func RemoveRunSubmitters(cur realm, addrs []string) {
223	assertDelegate(0, cur, runSubmittersMgr, "the "+runSubmittersKey+" allowlist")
224	if len(addrs) == 0 {
225		return
226	}
227	for _, a := range addrs {
228		if !runSubmittersGrants.Has(address(a)) {
229			panic("cannot remove " + a + ": not granted by " + runSubmittersMgr +
230				", only GovDAO may remove it")
231		}
232	}
233
234	// The delegate may not empty the list, whatever it granted.
235	//
236	// An empty run_submitters means the gate is OFF -- anyone on the chain may
237	// send MsgRun. So emptying it is not a smaller version of removing one
238	// address, it is the opposite of what the delegation is for: it would let a
239	// delegate authorized to curate a list unilaterally revoke the whole
240	// restriction GovDAO voted for.
241	//
242	// Grant-scoping alone does not prevent this. It holds only while at least
243	// one entry the delegate did not grant survives, and GovDAO replacing the
244	// list wholesale can remove its own entries without touching the grant
245	// record. A floor makes the invariant structural instead of emergent.
246	//
247	// Counted against the parameter, not the argument: the caller may name
248	// addresses that are not listed, or name one twice, and neither shrinks the
249	// list. Only GovDAO can go to zero, through the whole-list setter, where the
250	// resulting list is on the ballot.
251	removing := make(map[string]bool, len(addrs))
252	for _, a := range addrs {
253		removing[a] = true
254	}
255	remaining := 0
256	for _, a := range GetRunSubmitters() {
257		if !removing[a] {
258			remaining++
259		}
260	}
261	if remaining == 0 {
262		panic("refusing to empty the " + runSubmittersKey +
263			" allowlist: an empty list disables the gate entirely, so only " +
264			"GovDAO may do it")
265	}
266
267	prms.UpdateSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey, addrs, false)
268	for _, a := range addrs {
269		runSubmittersGrants.Remove(address(a))
270	}
271	chain.Emit(DelegateWriteEvent,
272		"key", runSubmittersKey, "realm", runSubmittersMgr, "op", "remove")
273}
274
275// GetRunSubmitters returns the current allowlist.
276func GetRunSubmitters() []string {
277	vals, _ := prms.GetSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey)
278	return vals
279}
280
281// ProposeSetRunSubmitters creates a GovDAO proposal replacing the whole
282// run_submitters allowlist.
283//
284// This is the only way to set the list by vote: the generic factories refuse
285// the key (see assertNotRunSubmittersKey), so every whole-list write comes
286// through here and carries the rule below.
287//
288// The proposer must be on the list they propose.
289//
290// An empty run_submitters means the gate is off and anyone may MsgRun. A
291// non-empty one therefore decides who may run code at all -- and a list naming
292// nobody who can create a GovDAO proposal cannot be undone, because creating a
293// proposal needs MsgRun: a ProposalRequest carries an Executor, and MsgCall
294// cannot build one from string arguments. The vote would end governance.
295//
296// Requiring the proposer's own address is a cheap way to prove the list is
297// usable rather than merely plausible. GovDAO refuses a proposal from a
298// non-member (PreCreateProposal, "only members can create new proposals"), so
299// if this proposal exists at all its author is a member -- and they just signed
300// the transaction that created it, so the address demonstrably holds a key. A
301// list that merely NAMES a member proves neither: the address may belong to
302// nobody, since any member can enroll an arbitrary address.
303//
304// Checked here, at proposal creation, rather than inside the executor. Nothing
305// in r/gov/dao recovers from an executor panic, so a check that fires at
306// execution turns a passed proposal into one that can never be executed. Here
307// the refusal reaches a person who can still fix the list and propose again.
308//
309// This is a floor, not an invariant. The proposer may resign from GovDAO later,
310// and the list is not re-checked when they do. It rules out arriving at a dead
311// list in one vote; it cannot rule out drifting into one.
312func ProposeSetRunSubmitters(cur realm, addrs []string) dao.ProposalRequest {
313	// IsCurrent before Previous, as AGENTS.md requires and assertDelegate does.
314	//
315	// Belt and braces here rather than load-bearing: the compiler already
316	// refuses anything but `cur` or `cross(rlm)` as the first argument to a
317	// crossing function, so this cur is live by construction and the check
318	// cannot fire. It earns its place if this ever becomes a helper taking an
319	// ordinary realm parameter, which is the shape assertDelegate has and where
320	// a stashed value really can be threaded in.
321	if !cur.IsCurrent() {
322		panic("unauthorized: cur is not the caller's live realm")
323	}
324	proposer := cur.Previous().Address()
325	listed := false
326	for _, a := range addrs {
327		if a == proposer.String() {
328			listed = true
329			break
330		}
331	}
332	if len(addrs) > 0 && !listed {
333		panic("refusing to propose a " + runSubmittersKey + " allowlist that omits " +
334			"the proposer " + proposer.String() + ": a non-empty list that names nobody " +
335			"who can create a proposal cannot be changed back")
336	}
337
338	title := "Set the " + runSubmittersKey + " allowlist"
339	desc := "Replace the " + runSubmittersKey + " allowlist, which gates who may " +
340		"send MsgRun."
341	if len(addrs) == 0 {
342		desc += " This empties the list, which switches the gate OFF: anyone may " +
343			"send MsgRun."
344	} else {
345		desc += " Only these addresses may send MsgRun. Creating a GovDAO proposal " +
346			"needs MsgRun, so this list also decides who can govern."
347		for _, a := range addrs {
348			desc += "\n- " + a
349		}
350	}
351
352	callback := func(cur realm) error {
353		setRunSubmitters(addrs)
354		chain.Emit("SetRunSubmitters", "key", runSubmittersKey, "proposer", proposer.String())
355		return nil
356	}
357	return dao.NewProposalRequest(title, desc,
358		dao.NewSimpleExecutor(0, cur, callback, ""))
359}
360
361// setRunSubmitters replaces the whole allowlist.
362//
363// SetSysParamStrings, not UpdateSysParamStrings: Update with add=true appends
364// non-duplicates onto what is already there, so it can never remove an address
365// or empty the list. This realm reserves the key from the generic factories, so
366// this is the only route by vote -- if it appended, the parameter would be
367// append-only chain-wide, a compromised address could never be de-listed, and
368// the gate could never be turned back off.
369//
370// A named function rather than the executor's body inline, so a test can reach
371// it: a closure held in a ProposalRequest cannot be called from outside.
372func setRunSubmitters(addrs []string) {
373	prms.SetSysParamStrings(vmModulePrefix, vmParamsSubmodule, runSubmittersKey, addrs)
374}