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}