Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

valopers.gno

25.12 Kb · 709 lines
  1// Package valopers is designed around the permissionless lifecycle of valoper profiles.
  2package valopers
  3
  4import (
  5	"chain"
  6	"chain/runtime"
  7	"chain/runtime/unsafe"
  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}