Kind Realm (renderable)
Name valopers
Namespace gnops
Exported functions 13
Module gno.land/r/gnops/valopers
gno 0.9 valopers.gno gno
⧉
1 // Package valopers is designed around the permissionless lifecycle of valoper profiles. 2 package valopers 3 4 import ( 5 "chain" 6 "chain/runtime" 7 "chain/runtime/unsafe" AddToAuthList (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, member string)
Auth () string
DeleteFromAuthList (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, member string)
GetByAddr (addr string) struct{Moniker string; Description string; ServerType string; OperatorAddress .uverse.address; SigningPubKey string; SigningAddress .uverse.address; LastRotationHeight int64; KeepRunning bool; auth *gno.land/p/nt/ownable/exts/authorizable/v0.Authorizable}
NewAuthorityRotationProposalRequest (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newContractPath string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
NewInstructionsProposalRequest (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newInstructions string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
Register (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, moniker string, description string, serverType string, addr string, pubKey string)
Render (fullPath string) string
UpdateDescription (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, description string)
UpdateKeepRunning (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, keepRunning bool)
UpdateMoniker (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, moniker string)
UpdateServerType (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, serverType string)
UpdateSigningKey (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, newPubKey string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
8
"crypto/bech32"
9 "errors"
10 "math"
11 "regexp"
12
13 "gno.land/p/moul/realmpath/v0"
14 "gno.land/p/nt/avl/pager/v0"
15 "gno.land/p/nt/avl/v0"
16 "gno.land/p/nt/bptree/v0"
17 "gno.land/p/nt/combinederr/v0"
18 "gno.land/p/nt/ownable/exts/authorizable/v0"
19 "gno.land/p/nt/ownable/v0"
20 "gno.land/p/nt/ufmt/v0"
21 sysparams "gno.land/r/sys/params"
22 validators "gno.land/r/sys/validators/v0"
23 )
24
25 const (
26 MonikerMaxLength = 32
27 DescriptionMaxLength = 2048
28
29 // Valid server types
30 ServerTypeCloud = "cloud"
31 ServerTypeOnPrem = "on-prem"
32 ServerTypeDataCenter = "data-center"
33 )
34
35 var (
36 ErrValoperExists = errors.New( "valoper already exists" )
37 ErrValoperMissing = errors.New( "valoper does not exist" )
38 ErrInvalidAddress = errors.New( "invalid address" )
39 ErrInvalidMoniker = errors.New( "moniker is not valid" )
40 ErrInvalidDescription = errors.New( "description is not valid" )
41 ErrInvalidServerType = errors.New( "server type is not valid" )
42 ErrOperatorSquatGuard = errors.New( "post-genesis: caller must equal operator address" )
43 ErrSigningKeyTaken = errors.New( "signing address already in registry (active or retired)" )
44 ErrFrontrunValidator = errors.New( "post-genesis: signing address is already an active validator" )
45 ErrRotationThrottled = errors.New( "rotation throttled: try again later" )
46 ErrRegistryEntryMissing = errors.New( "signing address has no active registry entry (corrupted state)" )
47 ErrPaidCallNotDirect = errors.New( "a fee is configured: call this directly as a user (maketx call), not from a realm or a maketx run script" )
48 ErrFeeParamOutOfRange = errors.New( "configured fee exceeds the maximum representable coin amount" )
49 ErrDisallowedPubKeyType = errors.New( "consensus pubkey type is not allowed for validators" )
50 )
51
52 var (
53 valopers *avl.Tree // operator-address -> Valoper
54 instructions string // markdown instructions for valoper's registration
55
56 // signingRegistry maps SigningAddress.String() -> regEntry.
57 // Permanently retains retired entries to prevent key reuse and
58 // to support future slashing-attribution by signing address.
59 signingRegistry = bptree.NewBPTree32()
60
61 monikerMaxLengthMiddle = ufmt.Sprintf( "%d" , MonikerMaxLength- 2 )
62 validateMonikerRe = regexp.MustCompile( `^[a-zA-Z0-9][\w -]{0,` + monikerMaxLengthMiddle + `}[a-zA-Z0-9]$` ) // 32 characters, including spaces, hyphens or underscores in the middle
63 )
64
65 // regEntry tracks signing-address -> operator with retirement metadata.
66 // retiredAtHeight == 0 means the entry is currently active for the operator.
67 type regEntry struct {
68 OperatorAddress address
69 RegisteredAtHeight int64
70 RetiredAtHeight int64
71 }
72
73 // Valoper represents a validator operator profile.
74 type Valoper struct {
75 Moniker string // A human-readable name
76 Description string // A description and details about the valoper
77 ServerType string // The type of server (cloud/on-prem/data-center)
78
79 OperatorAddress address // operator identity, profile key, stable across rotations
80 SigningPubKey string // current consensus signing pubkey (bech32 gpub1...)
81 SigningAddress address // = chain.PubKeyAddress(SigningPubKey)
82
83 LastRotationHeight int64 // throttle anchor for UpdateSigningKey
84
85 KeepRunning bool // operator wants this validator running in the active set
86
87 auth *authorizable.Authorizable
88 }
89
90 // AuthOwner returns the operator address that owns this profile's auth
91 // list. Read-only by construction: it copies out an address rather than
92 // returning the live *authorizable.Authorizable.
93 //
94 // SECURITY: the previous `Auth() *authorizable.Authorizable` was a
95 // capability leak of the same class this realm's governance authority
96 // closes by returning a description instead of the live authority.
97 // `Valoper` is returned BY VALUE from the exported, non-crossing
98 // GetByAddr, but `auth` is a pointer field, so the copy shared the
99 // callee's Authorizable. Any realm could therefore obtain a live,
100 // mutable handle and write through it.
101 //
102 // That was a privilege escalation, not merely a wider surface.
103 // Authorizable's gates read `rlm.Previous().Address()`, so inside a
104 // hostile realm's frame `Previous()` is whoever called it. When a
105 // valoper OPERATOR called any function of a hostile realm (faucet,
106 // airdrop, mint), that realm could reach
107 // `GetByAddr(operator).Auth().AddToAuthList(...)` and — because
108 // Previous() was then the operator, the Authorizable's own owner —
109 // persist itself onto the operator's auth list. From the next
110 // transaction on it acted alone: UpdateKeepRunning to drain the
111 // validator, UpdateSigningKey to rotate the consensus key.
112 //
113 // The exported wrappers below are NOT equivalent to the raw handle and
114 // were never the hole: there `cur.Previous()` is the hostile realm
115 // itself rather than the operator, so the owner check rejects it.
116 func (v Valoper) AuthOwner() address {
117 return v.auth.Owner()
118 }
119
120 func AddToAuthList(cur realm, addr address, member address) {
121 v := GetByAddr(addr)
122 if err := v.auth.AddToAuthList( 0 , cur, member); err != nil {
123 panic(err)
124 }
125 }
126
127 func DeleteFromAuthList(cur realm, addr address, member address) {
128 v := GetByAddr(addr)
129 if err := v.auth.DeleteFromAuthList( 0 , cur, member); err != nil {
130 panic(err)
131 }
132 }
133
134 // Register registers a new valoper. The `addr` parameter is the
135 // operator address (stable identity, profile key); `pubKey` is the
136 // consensus signing pubkey, from which the signing address is derived.
137 //
138 // Auth shape:
139 // - Post-genesis: OriginCaller must equal addr (operator-slot squat
140 // guard). Genesis-mode replay (ChainHeight()==0) bypasses, so
141 // migration .jsonl txs and historical Register replays succeed.
142 // - Signing-address uniqueness: derived(pubKey) must not already be
143 // in signingRegistry, active or retired.
144 // - Front-running guard: post-genesis, derived(pubKey) must not
145 // already be an active validator (a fresh registration cannot
146 // squat on the consensus address of an existing validator).
147 //
148 // Why OriginCaller==addr is sufficient for the SQUAT guard (no
149 // IsUserCall): squatting requires the attacker to satisfy
150 // OriginCaller==victim, which requires the victim's signing key.
151 //
152 // The PAYMENT check is a different matter and does need IsUserCall —
153 // see assertPaidCallIsDirect. An earlier version of this comment
154 // claimed the fee was "validated against banker.OriginSend in a way
155 // that's symmetric to IsUserCall via direct comparison"; there was no
156 // such comparison, and the fee was bypassable (reported by @D4ryl00).
157 //
158 // Auth-list seeding: the profile's Authorizable owner is set to addr
159 // (NOT OriginCaller). At H>0 the squat guard makes them equal anyway;
160 // at H==0 the deployer pattern (one signer registers many operators)
161 // requires owner == addr so each operator can manage their own profile
162 // post-genesis without needing the deployer's auth.
163 func Register(cur realm, moniker string , description string , serverType string , addr address, pubKey string ) {
164 // Operator-slot squat guard.
165 if runtime.ChainHeight() > 0 && unsafe.OriginCaller() != addr {
166 panic(ErrOperatorSquatGuard)
167 }
168
169 // Fee enforcement (read from sysparams; defaults to 0 until
170 // governance raises it post-transfer-enablement).
171 if fee := sysparams.GetValoperRegisterFee(); fee > 0 {
172 assertPaidCallIsDirect( 0 , cur)
173 minFee := minFeeCoin(fee)
174 sentCoins := unsafe.OriginSend()
175 if len(sentCoins) != 1 || sentCoins[ 0 ].IsLT(minFee) {
176 panic(ufmt.Sprintf( "payment must not be less than %d%s" , minFee.Amount, minFee.Denom))
177 }
178 }
179
180 // Check if the valoper is already registered.
181 if isValoper(addr) {
182 panic(ErrValoperExists)
183 }
184
185 // Reject disallowed key types early (else the EndBlocker drops it silently).
186 assertPubKeyTypeAllowed(pubKey)
187
188 // Derive the consensus signing address from the pubkey.
189 signingAddr, err := chain.PubKeyAddress(pubKey)
190 if err != nil {
191 panic(err)
192 }
193
194 // Signing-address uniqueness across all profiles, ever.
195 if signingRegistry.Has(signingAddr.String()) {
196 panic(ErrSigningKeyTaken)
197 }
198
199 // Front-running guard: post-genesis, the signing address must
200 // not already be an active validator.
201 if runtime.ChainHeight() > 0 && validators.IsValidator(signingAddr) {
202 panic(ErrFrontrunValidator)
203 }
204
205 v := Valoper{
206 Moniker: moniker,
207 Description: description,
208 ServerType: serverType,
209 OperatorAddress: addr,
210 SigningPubKey: pubKey,
211 SigningAddress: signingAddr,
212 LastRotationHeight: runtime.ChainHeight(),
213 KeepRunning: true ,
214 auth: authorizable.New(ownable.NewWithAddress(addr)),
215 }
216
217 if err := v.Validate(); err != nil {
218 panic(err)
219 }
220
221 // Save the valoper to the set.
222 valopers.Set(v.OperatorAddress.String(), v)
223
224 // Insert into the signing-address registry.
225 signingRegistry.Set(signingAddr.String(), regEntry{
226 OperatorAddress: addr,
227 RegisteredAtHeight: runtime.ChainHeight(),
228 RetiredAtHeight: 0 ,
229 })
230
231 // Refresh v0's cache for this operator.
232 validators.NotifyValoperChanged(cross(cur), addr, v.SigningPubKey, v.SigningAddress, v.KeepRunning)
233 }
234
235 // UpdateMoniker updates an existing valoper's moniker.
236 func UpdateMoniker(cur realm, addr address, moniker string ) {
237 // Check that the moniker is not empty.
238 if err := validateMoniker(moniker); err != nil {
239 panic(err)
240 }
241
242 v := GetByAddr(addr)
243
244 // Check that the caller has permissions.
245 v.auth.AssertPreviousOnAuthList( 0 , cur)
246
247 // Update the moniker.
248 v.Moniker = moniker
249
250 // Save the valoper info.
251 valopers.Set(addr.String(), v)
252 }
253
254 // UpdateDescription updates an existing valoper's description.
255 func UpdateDescription(cur realm, addr address, description string ) {
256 // Check that the description is not empty.
257 if err := validateDescription(description); err != nil {
258 panic(err)
259 }
260
261 v := GetByAddr(addr)
262
263 // Check that the caller has permissions.
264 v.auth.AssertPreviousOnAuthList( 0 , cur)
265
266 // Update the description.
267 v.Description = description
268
269 // Save the valoper info.
270 valopers.Set(addr.String(), v)
271 }
272
273 // UpdateKeepRunning updates an existing valoper's active status.
274 // Calls v0.NotifyValoperChanged because the cache stores KeepRunning.
275 func UpdateKeepRunning(cur realm, addr address, keepRunning bool ) {
276 v := GetByAddr(addr)
277
278 // Check that the caller has permissions.
279 v.auth.AssertPreviousOnAuthList( 0 , cur)
280
281 // Update status.
282 v.KeepRunning = keepRunning
283
284 // Save the valoper info.
285 valopers.Set(addr.String(), v)
286
287 // Refresh v0's cache (KeepRunning is one of the cached fields).
288 validators.NotifyValoperChanged(cross(cur), addr, v.SigningPubKey, v.SigningAddress, v.KeepRunning)
289 }
290
291 // UpdateServerType updates an existing valoper's server type.
292 func UpdateServerType(cur realm, addr address, serverType string ) {
293 // Check that the server type is valid.
294 if err := validateServerType(serverType); err != nil {
295 panic(err)
296 }
297
298 v := GetByAddr(addr)
299
300 // Check that the caller has permissions.
301 v.auth.AssertPreviousOnAuthList( 0 , cur)
302
303 // Update server type.
304 v.ServerType = serverType
305
306 // Save the valoper info.
307 valopers.Set(addr.String(), v)
308 }
309
310 // UpdateSigningKey rotates an operator's consensus signing key.
311 //
312 // Auth: caller must be on the operator's auth list (defaults to
313 // operator at Register time; extendable via AddToAuthList).
314 //
315 // Invariants checked at entry:
316 // - throttle: ChainHeight() - v.LastRotationHeight >=
317 // rotationPeriodBlocks
318 // - signingRegistry uniqueness: derived(newPubKey) not in registry
319 // (active OR retired); permanently blocks key reuse
320 // - fee: unsafe.OriginSend() >= rotationFee (mirrors Register's
321 // fee-check pattern)
322 //
323 // Effect: profile's SigningPubKey/SigningAddress/LastRotationHeight
324 // updated; old registry entry marked retired (retiredAtHeight =
325 // ChainHeight()); new entry inserted into signingRegistry; v0 emits
326 // remove+add to sysparams via RotateValoperSigningKey; v0 cache
327 // refreshed via NotifyValoperChanged. Rotation lands in consensus
328 // at H+2.
329 //
330 // Atomicity: Gno tx atomicity rolls back all state if any step
331 // panics. If v0.RotateValoperSigningKey panics, the registry insert
332 // and profile mutation revert with it.
333 func UpdateSigningKey(cur realm, addr address, newPubKey string ) {
334 v := GetByAddr(addr)
335
336 // Auth: caller must be on operator's auth list.
337 v.auth.AssertPreviousOnAuthList( 0 , cur)
338
339 // Throttle: limit one rotation per rotation_period_blocks per
340 // operator (per profile, not per caller — multi-member auth lists
341 // can't multiplicative-rotate).
342 height := runtime.ChainHeight()
343 if height-v.LastRotationHeight < sysparams.GetValoperRotationPeriodBlocks() {
344 panic(ErrRotationThrottled)
345 }
346
347 // Fee: enforce only if non-zero (matches Register's pattern;
348 // rotation_fee defaults to zero pre-transfer-enablement).
349 if fee := sysparams.GetValoperRotationFee(); fee > 0 {
350 assertPaidCallIsDirect( 0 , cur)
351 minFee := minFeeCoin(fee)
352 sentCoins := unsafe.OriginSend()
353 if len(sentCoins) != 1 || sentCoins[ 0 ].IsLT(minFee) {
354 panic(ufmt.Sprintf( "payment must not be less than %d%s" , minFee.Amount, minFee.Denom))
355 }
356 }
357
358 // Reject disallowed key types early (else the EndBlocker drops it silently).
359 assertPubKeyTypeAllowed(newPubKey)
360
361 // Derive the new signing address from the new pubkey.
362 newSigningAddr, err := chain.PubKeyAddress(newPubKey)
363 if err != nil {
364 panic(err)
365 }
366
367 // signingRegistry uniqueness: new key must not have ever been
368 // registered (active or retired).
369 if signingRegistry.Has(newSigningAddr.String()) {
370 panic(ErrSigningKeyTaken)
371 }
372
373 // Front-running guard: the derived signing address must not already
374 // be an active validator. Mirrors the same guard in Register
375 // (ErrFrontrunValidator). signingRegistry uniqueness above only
376 // blocks signing addresses that previously went through Register or
377 // UpdateSigningKey — genesis-seeded validators bypassed both, so
378 // their signing addresses are absent from signingRegistry. Without
379 // this check, a valoper could rotate onto such a slot and hijack
380 // it: v0.RotateValoperSigningKey would overwrite the active entry
381 // with this operator's claim, and a subsequent govDAO remove-op
382 // proposal would then delete it.
383 if validators.IsValidator(newSigningAddr) {
384 panic(ErrFrontrunValidator)
385 }
386
387 // Remember the previous signing key for the v0 cross-call.
388 oldPubKey := v.SigningPubKey
389 oldSigningAddr := v.SigningAddress
390
391 // Mark the old registry entry retired. The entry must exist —
392 // it was inserted at Register time.
393 rawOld := signingRegistry.Get(oldSigningAddr.String())
394 if rawOld == nil {
395 panic(ErrRegistryEntryMissing)
396 }
397 oldEntry := rawOld.(regEntry)
398 oldEntry.RetiredAtHeight = height
399 signingRegistry.Set(oldSigningAddr.String(), oldEntry)
400
401 // Insert the new entry as active.
402 signingRegistry.Set(newSigningAddr.String(), regEntry{
403 OperatorAddress: addr,
404 RegisteredAtHeight: height,
405 RetiredAtHeight: 0 ,
406 })
407
408 // Update the profile.
409 v.SigningPubKey = newPubKey
410 v.SigningAddress = newSigningAddr
411 v.LastRotationHeight = height
412 valopers.Set(addr.String(), v)
413
414 // Apply to consensus via v0, then refresh v0's cache view of the
415 // profile. Order matters only in that both must complete; tx
416 // atomicity rolls back together on any panic.
417 validators.RotateValoperSigningKey(cross(cur), addr, oldPubKey, newPubKey)
418 validators.NotifyValoperChanged(cross(cur), addr, v.SigningPubKey, v.SigningAddress, v.KeepRunning)
419 }
420
421 // GetByAddr fetches the valoper using the operator address, if present.
422 func GetByAddr(addr address) Valoper {
423 valoperRaw := valopers.Get(addr.String())
424 if valoperRaw == nil {
425 panic(ErrValoperMissing)
426 }
427
428 return valoperRaw.(Valoper)
429 }
430
431 // Render renders the current valoper set.
432 // "/r/gnops/valopers" lists all valopers, paginated.
433 // "/r/gnops/valopers:addr" shows the detail for the valoper with the addr.
434 func Render(fullPath string ) string {
435 req := realmpath.Parse(fullPath)
436 if req.Path == "" {
437 return renderHome(fullPath)
438 } else {
439 addr := req.Path
440 if len(addr) < 2 || addr[: 2 ] != "g1" {
441 return "invalid address " + addr
442 }
443 valoperRaw := valopers.Get(addr)
444 if valoperRaw == nil {
445 return "unknown address " + addr
446 }
447 v := valoperRaw.(Valoper)
448 return "Valoper's details:\n" + v.Render()
449 }
450 }
451
452 func renderHome(path string ) string {
453 // if there are no valopers, display instructions
454 if valopers.Size() == 0 {
455 return ufmt.Sprintf( "%s\n\nNo valopers to display." , instructions)
456 }
457
458 page := pager.NewPager(valopers, 50 , false ).MustGetPageByPath(path)
459
460 output := ""
461
462 // if we are on the first page, display instructions
463 if page.PageNumber == 1 {
464 output += ufmt.Sprintf( "%s\n\n" , instructions)
465 }
466
467 for _, item := range page.Items {
468 v := item.Value.(Valoper)
469 output += ufmt.Sprintf( " * [%s](/r/gnops/valopers:%s) - [profile](/r/demo/profile:u/%s)\n" ,
470 v.Moniker, v.OperatorAddress, v.OperatorAddress)
471 }
472
473 output += "\n"
474 output += page.Picker(path)
475 return output
476 }
477
478 // Validate checks if the fields of the Valoper are valid.
479 func (v *Valoper) Validate() error {
480 errs := &combinederr.CombinedError{}
481
482 errs.Add(validateMoniker(v.Moniker))
483 errs.Add(validateDescription(v.Description))
484 errs.Add(validateServerType(v.ServerType))
485 errs.Add(validateBech32(v.OperatorAddress))
486 errs.Add(validatePubKey(v.SigningPubKey))
487
488 if errs.Size() == 0 {
489 return nil
490 }
491
492 return errs
493 }
494
495 // Render renders a single valoper with their information.
496 func (v Valoper) Render() string {
497 output := ufmt.Sprintf( "## %s\n" , v.Moniker)
498
499 if v.Description != "" {
500 output += ufmt.Sprintf( "%s\n\n" , v.Description)
501 }
502
503 output += ufmt.Sprintf( "- Operator Address: %s\n" , v.OperatorAddress.String())
504 output += ufmt.Sprintf( "- Signing Address: %s\n" , v.SigningAddress.String())
505 output += ufmt.Sprintf( "- Signing PubKey: %s\n" , v.SigningPubKey)
506 output += ufmt.Sprintf( "- Server Type: %s\n\n" , v.ServerType)
507 output += ufmt.Sprintf( "[Profile link](/r/demo/profile:u/%s)\n" , v.OperatorAddress)
508
509 return output
510 }
511
512 // isValoper checks if the valoper exists.
513 func isValoper(addr address) bool {
514 return valopers.Has(addr.String())
515 }
516
517 // validateMoniker checks if the moniker is valid.
518 func validateMoniker(moniker string ) error {
519 if moniker == "" {
520 return ErrInvalidMoniker
521 }
522
523 if len(moniker) > MonikerMaxLength {
524 return ErrInvalidMoniker
525 }
526
527 if !validateMonikerRe.MatchString(moniker) {
528 return ErrInvalidMoniker
529 }
530
531 return nil
532 }
533
534 // validateDescription checks if the description is valid.
535 func validateDescription(description string ) error {
536 if description == "" {
537 return ErrInvalidDescription
538 }
539
540 if len(description) > DescriptionMaxLength {
541 return ErrInvalidDescription
542 }
543
544 return nil
545 }
546
547 // validateBech32 checks if the value is a valid bech32 address.
548 func validateBech32(addr address) error {
549 if !addr.IsValid() {
550 return ErrInvalidAddress
551 }
552
553 return nil
554 }
555
556 // validatePubKey checks if the public key is valid.
557 func validatePubKey(pubKey string ) error {
558 if _, _, err := bech32.DecodeNoLimit(pubKey); err != nil {
559 return err
560 }
561
562 return nil
563 }
564
565 // assertPaidCallIsDirect requires the immediate caller to be a plain EOA,
566 // and must be paired with every unsafe.OriginSend() amount check in this
567 // realm. Called only when a fee is actually configured.
568 //
569 // unsafe.OriginSend() reports the transaction's declared send ENVELOPE, not
570 // what this realm received. Only a direct `maketx call` on valopers credits
571 // the envelope to this realm's address. For a `maketx run` the keeper sets
572 // pkgAddr := caller (gno.land/pkg/sdk/vm/keeper.go), so the coins move from
573 // the caller to the caller and never land anywhere at all, while
574 // OriginSend() still reports the full amount to us. See
575 // gno.land/adr/pr6062_payable_send_check.md: "MsgRun is exempt: the coins
576 // are moved from the caller to the caller, so nothing actually moves." So
577 // the amount check alone verifies intent, never receipt.
578 //
579 // Reported by @D4ryl00, with a local-chain reproduction:
580 // governance sets register_fee to 1000ugnot, an operator runs a script
581 // calling Register with `-send 1000ugnot`, registration succeeds, valopers
582 // receives zero, and the operator still holds the coins. No hostile script
583 // is needed — the envelope is free by construction. Both fees default to 0,
584 // so nothing was live.
585 //
586 // Why IsUserCall and not an address comparison: a run script's realm is
587 // address-INDISTINGUISHABLE from its user. Inside `main(cur realm)` the
588 // ephemeral realm's own address IS the caller's EOA address, so every
589 // address-based guard in this realm accepts a `maketx run` — Register's
590 // squat guard (OriginCaller == addr) and UpdateSigningKey's auth-list check
591 // (Previous().Address() on the list) both pass. Only the pkgPath differs,
592 // and IsUserCall() is the check that reads it. It is therefore the only
593 // PreviousRealm shape where the envelope is guaranteed to have landed here:
594 // it excludes intermediate code realms and user-run ephemeral realms alike.
595 // Same reasoning and same pairing as r/sys/namereg/v0.Register, which reads
596 // OriginSend for its anti-squatting payment; see the two-guard comment there.
597 //
598 // Deliberately inside the `fee > 0` branch rather than at the top of the
599 // function: while a fee is unset there is no payment to establish receipt
600 // of, and gating unconditionally would break the operator-authored
601 // `maketx run` flows that the squat guard is happy to accept. The
602 // restriction appears exactly when, and only when, money is involved.
603 //
604 // KNOWN LIMIT. AddToAuthList takes a plain `member address`, so a REALM
605 // can sit on an operator's auth list, and once rotation_fee is nonzero
606 // such a member can no longer drive UpdateSigningKey — it is not a user
607 // call. An EOA rotation bot is unaffected, which is the shape the
608 // auth-list docs describe ("HSM-bound or rotation-bot"), so nothing
609 // documented breaks; a realm-mediated rotation would.
610 //
611 // The alternative that would keep realms working is a true receipt check:
612 // track cumulative fees in realm state and require the realm's own
613 // balance to have risen by `fee` since the last one. That works for any
614 // caller shape, but it puts new persisted state and a banker into a
615 // genesis realm, and lets anyone pre-pay another operator's fee by
616 // donating to the realm address. Not worth it while both fees are 0 and
617 // collected fees are unwithdrawable anyway (this realm has no banker, so
618 // they strand at its address). Revisit if a realm-mediated paid rotation
619 // is ever actually wanted.
620 func assertPaidCallIsDirect(_ int , rlm realm) {
621 // IsCurrent before Previous, as AGENTS.md requires and every other
622 // (_ int, rlm realm) helper in this tree does (authorizable,
623 // sys/params/delegate, gov/dao/types, sys/validators/v0/cache).
624 // Both call sites pass their live cur, so this is defense-in-depth
625 // against a future caller threading a stale or stashed realm value:
626 // Previous() reads the prev field verbatim, so on a non-live token
627 // it answers for the wrong frame.
628 if !rlm.IsCurrent() {
629 panic(ErrPaidCallNotDirect)
630 }
631 if !rlm.Previous().IsUserCall() {
632 panic(ErrPaidCallNotDirect)
633 }
634 }
635
636 // minFeeCoin converts a configured fee (uint64, from sysparams) into the
637 // ugnot Coin the amount checks compare against.
638 //
639 // Coin.Amount is an int64. A fee above MaxInt64 wraps negative, and
640 // `sentCoins[0].IsLT(minFee)` is then false for every payment — the realm
641 // reads the fee as configured, runs assertPaidCallIsDirect, and collects
642 // 1ugnot while reporting "payment must not be less than -1ugnot". Failing
643 // closed here keeps an unrepresentable fee from opening the paid path
644 // instead of closing it.
645 //
646 // Reachable two ways today, neither requiring malice: governance can set
647 // any uint64 through the generic r/sys/params factories, and
648 // valopers/proposal.ProposeNewMinFeeProposalRequest takes an int64 and
649 // stores uint64(newMinFee), so a negative fee — the obvious way to write
650 // "disable the fee" — round-trips to 2^64-1. That signature is preserved
651 // for historical-replay compatibility and is deliberately left alone; the
652 // bound belongs at the consumption site, which covers both paths.
653 func minFeeCoin(fee uint64 ) chain.Coin {
654 if fee > math.MaxInt64 {
655 panic(ErrFeeParamOutOfRange)
656 }
657 return chain.NewCoin( "ugnot" , int64 (fee))
658 }
659
660 // assertPubKeyTypeAllowed panics if pubKey's type is not in the chain's validator allow-list (empty list accepts any).
661 func assertPubKeyTypeAllowed(pubKey string ) {
662 allowed := sysparams.GetValsetPubKeyTypes()
663 if len(allowed) == 0 {
664 return
665 }
666 typeURL, err := pubKeyTypeURL(pubKey)
667 if err != nil {
668 panic(err)
669 }
670 for _, a := range allowed {
671 if a == typeURL {
672 return
673 }
674 }
675 panic(ErrDisallowedPubKeyType)
676 }
677
678 // pubKeyTypeURL returns the amino type URL (e.g. "/tm.PubKeyEd25519") of a bech32 consensus pubkey.
679 func pubKeyTypeURL(pubKey string ) ( string , error ) {
680 // gpub exceeds bech32's 90-char cap, so decode without the limit.
681 _, data5, err := bech32.DecodeNoLimit(pubKey)
682 if err != nil {
683 return "" , err
684 }
685 data, err := bech32.ConvertBits(data5, 5 , 8 , false )
686 if err != nil {
687 return "" , err
688 }
689 // Type URL is the first amino field: 0x0A <len> <typeURL>.
690 if len(data) < 2 || data[ 0 ] != 0x0A {
691 return "" , errors.New( "malformed consensus pubkey: " + pubKey)
692 }
693 n := int (data[ 1 ])
694 if n == 0 || len(data) < 2 +n {
695 return "" , errors.New( "malformed consensus pubkey: " + pubKey)
696 }
697 return string (data[ 2 : 2 +n]), nil
698 }
699
700 // validateServerType checks if the server type is valid.
701 func validateServerType(serverType string ) error {
702 if serverType != ServerTypeCloud &&
703 serverType != ServerTypeOnPrem &&
704 serverType != ServerTypeDataCenter {
705 return ErrInvalidServerType
706 }
707
708 return nil
709 }
710
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.