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}