Skip to content

Commit

Permalink
feat: improve r/gnoland/valopers implementation (#2509)
Browse files Browse the repository at this point in the history
## Description

This PR improves the gno implementation of the `valopers` Realm.

Waiting on #2380 to add a context-based Govdao pattern before merging.

<details><summary>Contributors' checklist...</summary>

- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>

---------

Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com>
  • Loading branch information
zivkovicmilos and moul committed Jul 6, 2024
1 parent 30c6368 commit 0c69db3
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 21 deletions.
7 changes: 6 additions & 1 deletion examples/gno.land/r/gnoland/valopers/gno.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
module gno.land/r/gnoland/valopers

require (
gno.land/p/demo/ownable v0.0.0-latest
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/demo/testutils v0.0.0-latest
gno.land/p/demo/uassert v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/sys/validators v0.0.0-latest
gno.land/r/gov/dao v0.0.0-latest
gno.land/r/sys/validators v0.0.0-latest
)
7 changes: 7 additions & 0 deletions examples/gno.land/r/gnoland/valopers/init.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package valopers

import "gno.land/p/demo/avl"

func init() {
valopers = avl.NewTree()
}
182 changes: 162 additions & 20 deletions examples/gno.land/r/gnoland/valopers/valopers.gno
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,177 @@ package valopers
import (
"std"

"gno.land/p/demo/ownable"
"gno.land/p/demo/avl"
"gno.land/p/demo/ufmt"
pVals "gno.land/p/sys/validators"
govdao "gno.land/r/gov/dao"
"gno.land/r/sys/validators"
)

// Valoper represents a validator operator profile.
const (
errValoperExists = "valoper already exists"
errValoperMissing = "valoper does not exist"
errInvalidAddressUpdate = "valoper updated address exists"
errValoperNotCaller = "valoper is not the caller"
)

// valopers keeps track of all the active validator operators
var valopers *avl.Tree // Address -> Valoper

// Valoper represents a validator operator profile
type Valoper struct {
ownable.Ownable // Embedding the Ownable type for ownership management.
Name string // the display name of the valoper
Description string // the description of the valoper

Address std.Address // The bech32 gno address of the validator
PubKey string // the bech32 public key of the validator
P2PAddresses []string // the publicly reachable P2P addresses of the validator
Active bool // flag indicating if the valoper is active
}

// Register registers a new valoper
func Register(v Valoper) {
// Check if the valoper is already registered
if isValoper(v.Address) {
panic(errValoperExists)
}

// TODO add address derivation from public key
// (when the laws of gno make it possible)

// Save the valoper to the set
valopers.Set(v.Address.String(), v)
}

DisplayName string // The display name of the valoper.
ValidatorAddr std.Address // The address of the validator.
// TODO: Add other valoper metadata as needed.
// Update updates an existing valoper
func Update(address std.Address, v Valoper) {
// Check if the valoper is present
if !isValoper(address) {
panic(errValoperMissing)
}

// Check that the valoper wouldn't be
// overwriting an existing one
isAddressUpdate := address != v.Address
if isAddressUpdate && isValoper(v.Address) {
panic(errInvalidAddressUpdate)
}

// Remove the old valoper info
// in case the address changed
if address != v.Address {
valopers.Remove(address.String())
}

// Save the new valoper info
valopers.Set(v.Address.String(), v)
}

// Register registers a new valoper.
// TODO: Define the parameters and implement the function.
func Register( /* TBD */ ) {
panic("not implemented")
// GetByAddr fetches the valoper using the address, if present
func GetByAddr(address std.Address) Valoper {
valoperRaw, exists := valopers.Get(address.String())
if !exists {
panic(errValoperMissing)
}

return valoperRaw.(Valoper)
}

// Update updates an existing valoper.
// TODO: Define the parameters and implement the function.
func Update( /* TBD */ ) {
panic("not implemented")
// Render renders the current valoper set
func Render(_ string) string {
if valopers.Size() == 0 {
return "No valopers to display."
}

output := "Valset changes to apply:\n"
valopers.Iterate("", "", func(_ string, value interface{}) bool {
valoper := value.(Valoper)

output += valoper.Render()

return false
})

return output
}

// GovXXX is a placeholder for a function to interact with the governance DAO.
// TODO: Define a good API and implement it.
func GovXXX() {
// Assert that the caller is a member of the governance DAO.
govdao.AssertIsMember(std.PrevRealm().Addr())
panic("not implemented")
// Render renders a single valoper with their information
func (v Valoper) Render() string {
output := ufmt.Sprintf("## %s\n", v.Name)
output += ufmt.Sprintf("%s\n\n", v.Description)
output += ufmt.Sprintf("- Address: %s\n", v.Address.String())
output += ufmt.Sprintf("- PubKey: %s\n", v.PubKey)
output += "- P2P Addresses: [\n"

if len(v.P2PAddresses) == 0 {
output += "]\n"

return output
}

for index, addr := range v.P2PAddresses {
output += addr

if index == len(v.P2PAddresses)-1 {
output += "]\n"

continue
}

output += ",\n"
}

return output
}

// isValoper checks if the valoper exists
func isValoper(address std.Address) bool {
_, exists := valopers.Get(address.String())

return exists
}

// GovDAOProposal creates a proposal to the GovDAO
// for adding the given valoper to the validator set.
// This function is meant to serve as a helper
// for generating the govdao proposal
func GovDAOProposal(address std.Address) {
var (
valoper = GetByAddr(address)
votingPower = uint64(1)
)

// Make sure the valoper is the caller
if std.GetOrigCaller() != address {
panic(errValoperNotCaller)
}

// Determine the voting power
if !valoper.Active {
votingPower = uint64(0)
}

changesFn := func() []pVals.Validator {
return []pVals.Validator{
{
Address: valoper.Address,
PubKey: valoper.PubKey,
VotingPower: votingPower,
},
}
}

// Create the executor
executor := validators.NewPropExecutor(changesFn)

// Craft the proposal comment
comment := ufmt.Sprintf(
"Proposal to add valoper %s (Address: %s; PubKey: %s) to the valset",
valoper.Name,
valoper.Address.String(),
valoper.PubKey,
)

// Create the govdao proposal
govdao.Propose(comment, executor)
}
149 changes: 149 additions & 0 deletions examples/gno.land/r/gnoland/valopers/valopers_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package valopers

import (
"testing"

"gno.land/p/demo/avl"
"gno.land/p/demo/testutils"
"gno.land/p/demo/uassert"
)

func TestValopers_Register(t *testing.T) {
t.Parallel()

t.Run("already a valoper", func(t *testing.T) {
t.Parallel()

// Clear the set for the test
valopers = avl.NewTree()

v := Valoper{
Address: testutils.TestAddress("valoper"),
}

// Add the valoper
valopers.Set(v.Address.String(), v)

uassert.PanicsWithMessage(t, errValoperExists, func() {
Register(v)
})
})

t.Run("successful registration", func(t *testing.T) {
t.Parallel()

// Clear the set for the test
valopers = avl.NewTree()

v := Valoper{
Address: testutils.TestAddress("valoper"),
Name: "new valoper",
PubKey: "pub key",
}

uassert.NotPanics(t, func() {
Register(v)
})

uassert.NotPanics(t, func() {
valoper := GetByAddr(v.Address)

uassert.Equal(t, v.Address, valoper.Address)
uassert.Equal(t, v.Name, valoper.Name)
uassert.Equal(t, v.PubKey, valoper.PubKey)
})
})
}

func TestValopers_Update(t *testing.T) {
t.Parallel()

t.Run("non-existing valoper", func(t *testing.T) {
t.Parallel()

// Clear the set for the test
valopers = avl.NewTree()

v := Valoper{}

// Update the valoper
uassert.PanicsWithMessage(t, errValoperMissing, func() {
Update(v.Address, v)
})
})

t.Run("overwrite valoper", func(t *testing.T) {
t.Parallel()

// Clear the set for the test
valopers = avl.NewTree()

one := Valoper{
Address: testutils.TestAddress("valoper 1"),
}

// Add the valoper
uassert.NotPanics(t, func() {
Register(one)
})

initialAddress := testutils.TestAddress("valoper 2")
two := Valoper{
Address: initialAddress,
}

// Add the valoper
uassert.NotPanics(t, func() {
Register(two)
})

// Update the valoper address
// so it overlaps
two = Valoper{
Address: one.Address,
}

// Update the valoper
uassert.PanicsWithMessage(t, errInvalidAddressUpdate, func() {
Update(initialAddress, two)
})
})

t.Run("successful update", func(t *testing.T) {
t.Parallel()

// Clear the set for the test
valopers = avl.NewTree()

var (
name = "new valoper"
v = Valoper{
Address: testutils.TestAddress("valoper"),
Name: name,
PubKey: "pub key",
}
)

// Add the valoper
uassert.NotPanics(t, func() {
Register(v)
})

// Update the valoper name
v.Name = "new name"
v.Active = false

// Update the valoper
uassert.NotPanics(t, func() {
Update(v.Address, v)
})

// Make sure the valoper is updated
uassert.NotPanics(t, func() {
valoper := GetByAddr(v.Address)

uassert.Equal(t, v.Name, valoper.Name)
uassert.Equal(t, v.Active, valoper.Active)
})
})
}

0 comments on commit 0c69db3

Please sign in to comment.