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