Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}