Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}