valopers.gno
8.86 Kb · 342 lines
1// Package valopers is designed around the permissionless lifecycle of valoper profiles.
2package valopers
3
4import (
5 "chain"
6 "chain/banker"
7 "crypto/bech32"
8 "errors"
9 "regexp"
10
11 "gno.land/p/moul/realmpath"
12 "gno.land/p/nt/avl/v0"
13 "gno.land/p/nt/avl/v0/pager"
14 "gno.land/p/nt/combinederr/v0"
15 "gno.land/p/nt/ownable/v0"
16 "gno.land/p/nt/ownable/v0/exts/authorizable"
17 "gno.land/p/nt/ufmt/v0"
18)
19
20const (
21 MonikerMaxLength = 32
22 DescriptionMaxLength = 2048
23
24 // Valid server types
25 ServerTypeCloud = "cloud"
26 ServerTypeOnPrem = "on-prem"
27 ServerTypeDataCenter = "data-center"
28)
29
30var (
31 ErrValoperExists = errors.New("valoper already exists")
32 ErrValoperMissing = errors.New("valoper does not exist")
33 ErrInvalidAddress = errors.New("invalid address")
34 ErrInvalidMoniker = errors.New("moniker is not valid")
35 ErrInvalidDescription = errors.New("description is not valid")
36 ErrInvalidServerType = errors.New("server type is not valid")
37)
38
39var (
40 valopers *avl.Tree // valopers keeps track of all the valoper profiles. Address -> Valoper
41 instructions string // markdown instructions for valoper's registration
42 minFee = chain.NewCoin("ugnot", 20*1_000_000) // minimum gnot must be paid to register.
43
44 monikerMaxLengthMiddle = ufmt.Sprintf("%d", MonikerMaxLength-2)
45 validateMonikerRe = regexp.MustCompile(`^[a-zA-Z0-9][\w -]{0,` + monikerMaxLengthMiddle + `}[a-zA-Z0-9]$`) // 32 characters, including spaces, hyphens or underscores in the middle
46)
47
48// Valoper represents a validator operator profile
49type Valoper struct {
50 Moniker string // A human-readable name
51 Description string // A description and details about the valoper
52 ServerType string // The type of server (cloud/on-prem/data-center)
53
54 Address address // The bech32 gno address of the validator
55 PubKey string // The bech32 public key of the validator
56 KeepRunning bool // Flag indicating if the owner wants to keep the validator running
57
58 auth *authorizable.Authorizable // The authorizer system for the valoper
59}
60
61func (v Valoper) Auth() *authorizable.Authorizable {
62 return v.auth
63}
64
65func AddToAuthList(cur realm, address_XXX address, member address) {
66 v := GetByAddr(address_XXX)
67 if err := v.Auth().AddToAuthList(member); err != nil {
68 panic(err)
69 }
70}
71
72func DeleteFromAuthList(cur realm, address_XXX address, member address) {
73 v := GetByAddr(address_XXX)
74 if err := v.Auth().DeleteFromAuthList(member); err != nil {
75 panic(err)
76 }
77}
78
79// Register registers a new valoper
80func Register(cur realm, moniker string, description string, serverType string, address_XXX address, pubKey string) {
81 // Check if a fee is enforced
82 if !minFee.IsZero() {
83 sentCoins := banker.OriginSend()
84
85 // Coins must be sent and cover the min fee
86 if len(sentCoins) != 1 || sentCoins[0].IsLT(minFee) {
87 panic(ufmt.Sprintf("payment must not be less than %d%s", minFee.Amount, minFee.Denom))
88 }
89 }
90
91 // Check if the valoper is already registered
92 if isValoper(address_XXX) {
93 panic(ErrValoperExists)
94 }
95
96 v := Valoper{
97 Moniker: moniker,
98 Description: description,
99 ServerType: serverType,
100 Address: address_XXX,
101 PubKey: pubKey,
102 KeepRunning: true,
103 auth: authorizable.New(ownable.NewWithOrigin()),
104 }
105
106 if err := v.Validate(); err != nil {
107 panic(err)
108 }
109
110 // TODO add address derivation from public key
111 // (when the laws of gno make it possible)
112
113 // Save the valoper to the set
114 valopers.Set(v.Address.String(), v)
115}
116
117// UpdateMoniker updates an existing valoper's moniker
118func UpdateMoniker(cur realm, address_XXX address, moniker string) {
119 // Check that the moniker is not empty
120 if err := validateMoniker(moniker); err != nil {
121 panic(err)
122 }
123
124 v := GetByAddr(address_XXX)
125
126 // Check that the caller has permissions
127 v.Auth().AssertPreviousOnAuthList()
128
129 // Update the moniker
130 v.Moniker = moniker
131
132 // Save the valoper info
133 valopers.Set(address_XXX.String(), v)
134}
135
136// UpdateDescription updates an existing valoper's description
137func UpdateDescription(cur realm, address_XXX address, description string) {
138 // Check that the description is not empty
139 if err := validateDescription(description); err != nil {
140 panic(err)
141 }
142
143 v := GetByAddr(address_XXX)
144
145 // Check that the caller has permissions
146 v.Auth().AssertPreviousOnAuthList()
147
148 // Update the description
149 v.Description = description
150
151 // Save the valoper info
152 valopers.Set(address_XXX.String(), v)
153}
154
155// UpdateKeepRunning updates an existing valoper's active status
156func UpdateKeepRunning(cur realm, address_XXX address, keepRunning bool) {
157 v := GetByAddr(address_XXX)
158
159 // Check that the caller has permissions
160 v.Auth().AssertPreviousOnAuthList()
161
162 // Update status
163 v.KeepRunning = keepRunning
164
165 // Save the valoper info
166 valopers.Set(address_XXX.String(), v)
167}
168
169// UpdateServerType updates an existing valoper's server type
170func UpdateServerType(cur realm, address_XXX address, serverType string) {
171 // Check that the server type is valid
172 if err := validateServerType(serverType); err != nil {
173 panic(err)
174 }
175
176 v := GetByAddr(address_XXX)
177
178 // Check that the caller has permissions
179 v.Auth().AssertPreviousOnAuthList()
180
181 // Update server type
182 v.ServerType = serverType
183
184 // Save the valoper info
185 valopers.Set(address_XXX.String(), v)
186}
187
188// GetByAddr fetches the valoper using the address, if present
189func GetByAddr(address_XXX address) Valoper {
190 valoperRaw, exists := valopers.Get(address_XXX.String())
191 if !exists {
192 panic(ErrValoperMissing)
193 }
194
195 return valoperRaw.(Valoper)
196}
197
198// Render renders the current valoper set.
199// "/r/gnops/valopers" lists all valopers, paginated.
200// "/r/gnops/valopers:addr" shows the detail for the valoper with the addr.
201func Render(fullPath string) string {
202 req := realmpath.Parse(fullPath)
203 if req.Path == "" {
204 return renderHome(fullPath)
205 } else {
206 addr := req.Path
207 if len(addr) < 2 || addr[:2] != "g1" {
208 return "invalid address " + addr
209 }
210 valoperRaw, exists := valopers.Get(addr)
211 if !exists {
212 return "unknown address " + addr
213 }
214 v := valoperRaw.(Valoper)
215 return "Valoper's details:\n" + v.Render()
216 }
217}
218
219func renderHome(path string) string {
220 // if there are no valopers, display instructions
221 if valopers.Size() == 0 {
222 return ufmt.Sprintf("%s\n\nNo valopers to display.", instructions)
223 }
224
225 page := pager.NewPager(valopers, 50, false).MustGetPageByPath(path)
226
227 output := ""
228
229 // if we are on the first page, display instructions
230 if page.PageNumber == 1 {
231 output += ufmt.Sprintf("%s\n\n", instructions)
232 }
233
234 for _, item := range page.Items {
235 v := item.Value.(Valoper)
236 output += ufmt.Sprintf(" * [%s](/r/gnops/valopers:%s) - [profile](/r/demo/profile:u/%s)\n",
237 v.Moniker, v.Address, v.Address)
238 }
239
240 output += "\n"
241 output += page.Picker(path)
242 return output
243}
244
245// Validate checks if the fields of the Valoper are valid
246func (v *Valoper) Validate() error {
247 errs := &combinederr.CombinedError{}
248
249 errs.Add(validateMoniker(v.Moniker))
250 errs.Add(validateDescription(v.Description))
251 errs.Add(validateServerType(v.ServerType))
252 errs.Add(validateBech32(v.Address))
253 errs.Add(validatePubKey(v.PubKey))
254
255 if errs.Size() == 0 {
256 return nil
257 }
258
259 return errs
260}
261
262// Render renders a single valoper with their information
263func (v Valoper) Render() string {
264 output := ufmt.Sprintf("## %s\n", v.Moniker)
265
266 if v.Description != "" {
267 output += ufmt.Sprintf("%s\n\n", v.Description)
268 }
269
270 output += ufmt.Sprintf("- Address: %s\n", v.Address.String())
271 output += ufmt.Sprintf("- PubKey: %s\n", v.PubKey)
272 output += ufmt.Sprintf("- Server Type: %s\n\n", v.ServerType)
273 output += ufmt.Sprintf("[Profile link](/r/demo/profile:u/%s)\n", v.Address)
274
275 return output
276}
277
278// isValoper checks if the valoper exists
279func isValoper(address_XXX address) bool {
280 _, exists := valopers.Get(address_XXX.String())
281
282 return exists
283}
284
285// validateMoniker checks if the moniker is valid
286func validateMoniker(moniker string) error {
287 if moniker == "" {
288 return ErrInvalidMoniker
289 }
290
291 if len(moniker) > MonikerMaxLength {
292 return ErrInvalidMoniker
293 }
294
295 if !validateMonikerRe.MatchString(moniker) {
296 return ErrInvalidMoniker
297 }
298
299 return nil
300}
301
302// validateDescription checks if the description is valid
303func validateDescription(description string) error {
304 if description == "" {
305 return ErrInvalidDescription
306 }
307
308 if len(description) > DescriptionMaxLength {
309 return ErrInvalidDescription
310 }
311
312 return nil
313}
314
315// validateBech32 checks if the value is a valid bech32 address
316func validateBech32(address_XXX address) error {
317 if !address_XXX.IsValid() {
318 return ErrInvalidAddress
319 }
320
321 return nil
322}
323
324// validatePubKey checks if the public key is valid
325func validatePubKey(pubKey string) error {
326 if _, _, err := bech32.DecodeNoLimit(pubKey); err != nil {
327 return err
328 }
329
330 return nil
331}
332
333// validateServerType checks if the server type is valid
334func validateServerType(serverType string) error {
335 if serverType != ServerTypeCloud &&
336 serverType != ServerTypeOnPrem &&
337 serverType != ServerTypeDataCenter {
338 return ErrInvalidServerType
339 }
340
341 return nil
342}