Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}