Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve r/gnoland/valopers implementation #2509

Merged
merged 11 commits into from
Jul 6, 2024
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()
}
178 changes: 158 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,173 @@ 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"
errValoperInactive = "valoper is not active"
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) {
moul marked this conversation as resolved.
Show resolved Hide resolved
// 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)
moul marked this conversation as resolved.
Show resolved Hide resolved

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

// 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)
}

// 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)
}

DisplayName string // The display name of the valoper.
ValidatorAddr std.Address // The address of the validator.
// TODO: Add other valoper metadata as needed.
// 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
}

// Register registers a new valoper.
// TODO: Define the parameters and implement the function.
func Register( /* TBD */ ) {
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
}

// Update updates an existing valoper.
// TODO: Define the parameters and implement the function.
func Update( /* TBD */ ) {
panic("not implemented")
// isValoper checks if the valoper exists
func isValoper(address std.Address) bool {
_, exists := valopers.Get(address.String())

return exists
}

// GovXXX is a placeholder for a function to interact with the governance DAO.
// TODO: Define a good API and implement it.
func GovXXX() {
moul marked this conversation as resolved.
Show resolved Hide resolved
// Assert that the caller is a member of the governance DAO.
govdao.AssertIsMember(std.PrevRealm().Addr())
panic("not implemented")
// 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) {
moul marked this conversation as resolved.
Show resolved Hide resolved
moul marked this conversation as resolved.
Show resolved Hide resolved
valoper := GetByAddr(address)
if !valoper.Active {
panic(errValoperInactive)
}

// Make sure the valoper is the caller
if std.GetOrigCaller() != address {
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
panic(errValoperInactive)
}

changesFn := func() []pVals.Validator {
return []pVals.Validator{
{
Address: valoper.Address,
PubKey: valoper.PubKey,
VotingPower: 10,
moul marked this conversation as resolved.
Show resolved Hide resolved
},
}
}

// 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)
})
})
}
Loading