PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/gnops/valopers

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
valopers
Namespace
gnops
Files
14 (gnomod.toml)
Exported functions
13
Module
gno.land/r/gnops/valopers
gno
0.9

Files (14)

  • gnomod.tomltoml
  • admin.gnogno
  • init.gnogno
  • valopers.gnogno
  • admin_test.gnogno
  • pubkey_type_test.gnogno
  • rotate_test.gnogno
  • valopers_test.gnogno
  • z_1_filetest.gnogno
  • z_2_filetest.gnogno
  • z_auth_readonly_filetest.gnogno
  • z_foreign_realm_capability_filetest.gnogno
  • z_govdao_only_principal_filetest.gnogno
  • z_operator_authlist_capability_filetest.gnogno
  • valopers.gnogno
    1// Package valopers is designed around the permissionless lifecycle of valoper profiles.2package valopers34import (5	"chain"6	"chain/runtime"7	"chain/runtime/unsafe"

    Functions

    • 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.

    Rendered

    RenderedRawgnoweb ↗
    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
    25const (
    26 MonikerMaxLength = 32
    27 DescriptionMaxLength = 2048
    28
    29 // Valid server types
    30 ServerTypeCloud = "cloud"
    31 ServerTypeOnPrem = "on-prem"
    32 ServerTypeDataCenter = "data-center"
    33)
    34
    35var (
    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
    52var (
    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.
    67type regEntry struct {
    68 OperatorAddress address
    69 RegisteredAtHeight int64
    70 RetiredAtHeight int64
    71}
    72
    73// Valoper represents a validator operator profile.
    74type 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.
    116func (v Valoper) AuthOwner() address {
    117 return v.auth.Owner()
    118}
    119
    120func 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
    127func 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.
    163func 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.
    236func 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.
    255func 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.
    275func 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.
    292func 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.
    333func 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.
    422func 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.
    434func 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
    452func 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.
    479func (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.
    496func (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.
    513func isValoper(addr address) bool {
    514 return valopers.Has(addr.String())
    515}
    516
    517// validateMoniker checks if the moniker is valid.
    518func 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.
    535func 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.
    548func 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.
    557func 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.
    620func 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.
    653func 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).
    661func 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.
    679func 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.
    701func 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.