From 0c69db3c346c6e2d28c3271b9cb88044bf60536c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Sat, 6 Jul 2024 17:32:12 +0200 Subject: [PATCH] feat: improve `r/gnoland/valopers` implementation (#2509) ## Description This PR improves the gno implementation of the `valopers` Realm. Waiting on #2380 to add a context-based Govdao pattern before merging.
Contributors' checklist... - [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).
--------- Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- examples/gno.land/r/gnoland/valopers/gno.mod | 7 +- examples/gno.land/r/gnoland/valopers/init.gno | 7 + .../gno.land/r/gnoland/valopers/valopers.gno | 182 ++++++++++++++++-- .../r/gnoland/valopers/valopers_test.gno | 149 ++++++++++++++ 4 files changed, 324 insertions(+), 21 deletions(-) create mode 100644 examples/gno.land/r/gnoland/valopers/init.gno create mode 100644 examples/gno.land/r/gnoland/valopers/valopers_test.gno diff --git a/examples/gno.land/r/gnoland/valopers/gno.mod b/examples/gno.land/r/gnoland/valopers/gno.mod index 96299ce6e35..2d24fb27952 100644 --- a/examples/gno.land/r/gnoland/valopers/gno.mod +++ b/examples/gno.land/r/gnoland/valopers/gno.mod @@ -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 ) diff --git a/examples/gno.land/r/gnoland/valopers/init.gno b/examples/gno.land/r/gnoland/valopers/init.gno new file mode 100644 index 00000000000..eea36fcf0ce --- /dev/null +++ b/examples/gno.land/r/gnoland/valopers/init.gno @@ -0,0 +1,7 @@ +package valopers + +import "gno.land/p/demo/avl" + +func init() { + valopers = avl.NewTree() +} diff --git a/examples/gno.land/r/gnoland/valopers/valopers.gno b/examples/gno.land/r/gnoland/valopers/valopers.gno index e35eb749c8f..74cec941e0d 100644 --- a/examples/gno.land/r/gnoland/valopers/valopers.gno +++ b/examples/gno.land/r/gnoland/valopers/valopers.gno @@ -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) } diff --git a/examples/gno.land/r/gnoland/valopers/valopers_test.gno b/examples/gno.land/r/gnoland/valopers/valopers_test.gno new file mode 100644 index 00000000000..89544c46ee5 --- /dev/null +++ b/examples/gno.land/r/gnoland/valopers/valopers_test.gno @@ -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) + }) + }) +}