valopers source realm
Package valopers is designed around the permissionless lifecycle of valoper profiles.
Package valopers is designed around the permissionless lifecycle of valoper profiles.
1
1
var ErrValoperExists, ErrValoperMissing, ErrInvalidAddress, ErrInvalidMoniker, ErrInvalidDescription, ErrInvalidServerType, ErrOperatorSquatGuard, ErrSigningKeyTaken, ErrFrontrunValidator, ErrRotationThrottled, ErrRegistryEntryMissing, ErrPaidCallNotDirect, ErrFeeParamOutOfRange, ErrDisallowedPubKeyType
1var (
2 ErrValoperExists = errors.New("valoper already exists")
3 ErrValoperMissing = errors.New("valoper does not exist")
4 ErrInvalidAddress = errors.New("invalid address")
5 ErrInvalidMoniker = errors.New("moniker is not valid")
6 ErrInvalidDescription = errors.New("description is not valid")
7 ErrInvalidServerType = errors.New("server type is not valid")
8 ErrOperatorSquatGuard = errors.New("post-genesis: caller must equal operator address")
9 ErrSigningKeyTaken = errors.New("signing address already in registry (active or retired)")
10 ErrFrontrunValidator = errors.New("post-genesis: signing address is already an active validator")
11 ErrRotationThrottled = errors.New("rotation throttled: try again later")
12 ErrRegistryEntryMissing = errors.New("signing address has no active registry entry (corrupted state)")
13 ErrPaidCallNotDirect = errors.New("a fee is configured: call this directly as a user (maketx call), not from a realm or a maketx run script")
14 ErrFeeParamOutOfRange = errors.New("configured fee exceeds the maximum representable coin amount")
15 ErrDisallowedPubKeyType = errors.New("consensus pubkey type is not allowed for validators")
16)13
func AddToAuthList
crossing Actionfunc Auth
ActionAuth returns a read-only description of the realm's current governance authority (for rendering / inspection). It renders as
Example
1contract_authority[contract=gno.land/r/gov/dao,proposer=contract-identity]
i.e. "GovDAO governs this realm, and only GovDAO may drive it" — a statement an on-chain reader can act on. The proposer half is load-bearing: without it the string is byte-identical whether the authority is gated or wide open (see ContractAuthority.String).
It deliberately does NOT return the live *authz.Authorizer: an exported handle to the authority would let any realm reach its mutators (Transfer, DoByPrevious, ...) directly. Returning a description instead of the handle keeps that surface unreachable from here in the first place. Valoper.AuthOwner follows the same rule for per-operator auth lists.
func DeleteFromAuthList
crossing Actionfunc NewAuthorityRotationProposalRequest
crossing Action1func NewAuthorityRotationProposalRequest(cur realm, newContractPath string) dao.ProposalRequestNewAuthorityRotationProposalRequest builds the GovDAO proposal that re-points this realm's governance authority at `newContractPath`.
WHY THIS EXISTS. Without it the authority installed by init.gno is permanently frozen: nothing else calls auth.Transfer, Auth() returns a description rather than the live Authorizer, and Transfer routes through the authority's own gate — so if the asserted principal ever stops being presentable, `instructions` becomes unwritable with no on-chain recovery. That is not hypothetical: the r/gov/dao proxy path is a governance artifact that can be superseded, and a well-formed but wrong path (say the impl path instead of the proxy) is accepted at construction and dead forever. Redeploying instead is expensive — r/sys/validators/v0/cache.gno hardcodes `const valopersRealmPath`, so a new pkgpath means editing a second genesis realm and re-registering every valoper. It must exist before deploy, because code cannot be added to a deployed realm.
WHY A PATH AND NOT AN authz.Authority. Taking an Authority would make this an open-interface input (Class-3 impl-substitution): a proposal could install an always-approve or always-deny authority, and readers of Auth() could not tell from the rendered description alone. Taking a path keeps the installed authority canonical by construction — always a ContractAuthority with the pass-through handler and the contract-identity gate — so the only thing governance can change is WHICH principal is asserted. The path is validated by authz.NewContractAuthority, which panics on a malformed one, aborting the proposal execution rather than bricking the realm.
This remains a privileged surface: a majority that can pass this proposal can point the authority at a principal it controls. That is the same power a majority already has over `instructions`, and it is the price of being recoverable at all.
func NewInstructionsProposalRequest
crossing ActionNewInstructionsProposalRequest builds the GovDAO proposal that rewrites this realm's instructions. It replaces the previously exported NewInstructionsProposalCallback.
SECURITY: the privileged closure must never leave this package.
The VM mints a crossing frame's `cur` from the CALLEE's declaring package, so a closure declared here always runs with valopers' identity no matter who invokes it — and whoever holds the closure chooses its Previous() by wrapping it in an executor of their own. Handing such a closure to a caller therefore hands out the ability to satisfy this realm's gate whichever principal it is pointed at: the handler in init.gno runs the action and `instructions` is rewritten with no proposal and no vote. That is what the exported callback did.
Returning a dao.ProposalRequest instead seals the capability: the executor is an unexported field with no accessor (dao.ProposalRequest exposes only Title/Description/Filter), so the only way to reach the closure is for GovDAO to execute the proposal that contains it.
Note this is deliberately stronger than returning a dao.Executor — even a dao.NewSafeExecutor-wrapped one. Executor.Execute is an exported method, so a returned Executor stays directly invocable by its holder, and SafeExecutor's only gate (InAllowedDAOs) FAILS OPEN while the allowedDAOs list is empty, which is the documented bootstrap state (see r/gov/dao/loader/v0). A sealed request has no such conditional.
The rule for this realm: no exported function may return a crossing closure, a dao.Executor, or anything else that carries valopers' frame identity — and no exported function may itself BE such a thing, i.e. have a signature assignable to dao's `func(realm) error` callback type, since the proxy would then invoke it on an attacker's behalf. filetests/z_foreign_realm_capability_filetest.gno pins the first; filetests/z_govdao_only_principal_filetest.gno pins the second.
func Register
crossing Action1func Register(cur realm, moniker string, description string, serverType string, addr address, pubKey string)Register registers a new valoper. The `addr` parameter is the operator address (stable identity, profile key); `pubKey` is the consensus signing pubkey, from which the signing address is derived.
Auth shape:
- Post-genesis: OriginCaller must equal addr (operator-slot squat guard). Genesis-mode replay (ChainHeight()==0) bypasses, so migration .jsonl txs and historical Register replays succeed.
- Signing-address uniqueness: derived(pubKey) must not already be in signingRegistry, active or retired.
- Front-running guard: post-genesis, derived(pubKey) must not already be an active validator (a fresh registration cannot squat on the consensus address of an existing validator).
Why OriginCaller==addr is sufficient for the SQUAT guard (no IsUserCall): squatting requires the attacker to satisfy OriginCaller==victim, which requires the victim's signing key.
The PAYMENT check is a different matter and does need IsUserCall — see assertPaidCallIsDirect. An earlier version of this comment claimed the fee was "validated against banker.OriginSend in a way that's symmetric to IsUserCall via direct comparison"; there was no such comparison, and the fee was bypassable (reported by @D4ryl00).
Auth-list seeding: the profile's Authorizable owner is set to addr (NOT OriginCaller). At H>0 the squat guard makes them equal anyway; at H==0 the deployer pattern (one signer registers many operators) requires owner == addr so each operator can manage their own profile post-genesis without needing the deployer's auth.
func Render
Render renders the current valoper set. "/r/gnops/valopers" lists all valopers, paginated. "/r/gnops/valopers:addr" shows the detail for the valoper with the addr.
func UpdateDescription
crossing ActionUpdateDescription updates an existing valoper's description.
func UpdateKeepRunning
crossing ActionUpdateKeepRunning updates an existing valoper's active status. Calls v0.NotifyValoperChanged because the cache stores KeepRunning.
func UpdateMoniker
crossing ActionUpdateMoniker updates an existing valoper's moniker.
func UpdateServerType
crossing ActionUpdateServerType updates an existing valoper's server type.
func UpdateSigningKey
crossing ActionUpdateSigningKey rotates an operator's consensus signing key.
Auth: caller must be on the operator's auth list (defaults to operator at Register time; extendable via AddToAuthList).
Invariants checked at entry:
- throttle: ChainHeight() - v.LastRotationHeight >= rotationPeriodBlocks
- signingRegistry uniqueness: derived(newPubKey) not in registry (active OR retired); permanently blocks key reuse
- fee: unsafe.OriginSend() >= rotationFee (mirrors Register's fee-check pattern)
Effect: profile's SigningPubKey/SigningAddress/LastRotationHeight updated; old registry entry marked retired (retiredAtHeight = ChainHeight()); new entry inserted into signingRegistry; v0 emits remove+add to sysparams via RotateValoperSigningKey; v0 cache refreshed via NotifyValoperChanged. Rotation lands in consensus at H+2.
Atomicity: Gno tx atomicity rolls back all state if any step panics. If v0.RotateValoperSigningKey panics, the registry insert and profile mutation revert with it.
func GetByAddr
ActionGetByAddr fetches the valoper using the operator address, if present.
1
type Valoper
struct 1type Valoper struct {
2 Moniker string // A human-readable name
3 Description string // A description and details about the valoper
4 ServerType string // The type of server (cloud/on-prem/data-center)
5
6 OperatorAddress address // operator identity, profile key, stable across rotations
7 SigningPubKey string // current consensus signing pubkey (bech32 gpub1...)
8 SigningAddress address // = chain.PubKeyAddress(SigningPubKey)
9
10 LastRotationHeight int64 // throttle anchor for UpdateSigningKey
11
12 KeepRunning bool // operator wants this validator running in the active set
13
14 auth *authorizable.Authorizable
15}Valoper represents a validator operator profile.
Methods on Valoper
func AuthOwner
method on ValoperAuthOwner returns the operator address that owns this profile's auth list. Read-only by construction: it copies out an address rather than returning the live *authorizable.Authorizable.
SECURITY: the previous `Auth() *authorizable.Authorizable` was a capability leak of the same class this realm's governance authority closes by returning a description instead of the live authority. `Valoper` is returned BY VALUE from the exported, non-crossing GetByAddr, but `auth` is a pointer field, so the copy shared the callee's Authorizable. Any realm could therefore obtain a live, mutable handle and write through it.
That was a privilege escalation, not merely a wider surface. Authorizable's gates read `rlm.Previous().Address()`, so inside a hostile realm's frame `Previous()` is whoever called it. When a valoper OPERATOR called any function of a hostile realm (faucet, airdrop, mint), that realm could reach `GetByAddr(operator).Auth().AddToAuthList(...)` and — because Previous() was then the operator, the Authorizable's own owner — persist itself onto the operator's auth list. From the next transaction on it acted alone: UpdateKeepRunning to drain the validator, UpdateSigningKey to rotate the consensus key.
The exported wrappers below are NOT equivalent to the raw handle and were never the hole: there `cur.Previous()` is the hostile realm itself rather than the operator, so the owner check rejects it.
func Render
method on ValoperRender renders a single valoper with their information.
func Validate
method on ValoperValidate checks if the fields of the Valoper are valid.
20
- chain stdlib
- chain/runtime stdlib
- chain/runtime/unsafe stdlib
- crypto/bech32 stdlib
- errors stdlib
- gno.land/p/moul/authz/v0 package
- gno.land/p/moul/realmpath/v0 package
- gno.land/p/moul/txlink/v0 package
- gno.land/p/nt/avl/pager/v0 package
- gno.land/p/nt/avl/v0 package
- gno.land/p/nt/bptree/v0 package
- gno.land/p/nt/combinederr/v0 package
- gno.land/p/nt/ownable/exts/authorizable/v0 package
- gno.land/p/nt/ownable/v0 package
- gno.land/p/nt/ufmt/v0 package
- gno.land/r/gov/dao realm
- gno.land/r/sys/params realm
- gno.land/r/sys/validators/v0 realm
- math stdlib
- regexp stdlib