Skip to content

Commit

Permalink
feat: add /r/sys/validators (#2130)
Browse files Browse the repository at this point in the history
## Description

This PR introduces an initial validator set implementation in Gno (realm
based), as outlined in #1824.

It introduces a Proof of Contribution validator set management
mechanism, based on govdao proposals.
Related PRs:
- #1945 
- #2344 

I've left the door open to arbitrary protocol implementations.

~I've also added 2 example implementations:~
- ~PoS (Proof of Stake) - users can stake funds (`ugnot`) to become part
of the on-chain validator set~
- ~PoA (Proof of Authority) - new validators need to be voted in by the
majority of the existing validator set~

Update: I've moved the example PoS + PoA implementation to another
unrelated PR, since they are out of scope

Closes #1824

<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 authored Jul 2, 2024
1 parent 608ca30 commit f6ca518
Show file tree
Hide file tree
Showing 14 changed files with 581 additions and 47 deletions.
10 changes: 10 additions & 0 deletions examples/gno.land/p/nt/poa/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module gno.land/p/nt/poa

require (
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/demo/urequire v0.0.0-latest
gno.land/p/sys/validators v0.0.0-latest
)
14 changes: 14 additions & 0 deletions examples/gno.land/p/nt/poa/option.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package poa

import "gno.land/p/sys/validators"

type Option func(*PoA)

// WithInitialSet sets the initial PoA validator set
func WithInitialSet(validators []validators.Validator) Option {
return func(p *PoA) {
for _, validator := range validators {
p.validators.Set(validator.Address.String(), validator)
}
}
}
106 changes: 106 additions & 0 deletions examples/gno.land/p/nt/poa/poa.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package poa

import (
"errors"
"std"

"gno.land/p/demo/avl"
"gno.land/p/sys/validators"
)

var ErrInvalidVotingPower = errors.New("invalid voting power")

// PoA specifies the Proof of Authority validator set, with simple add / remove constraints.
//
// To add:
// - proposed validator must not be part of the set already
// - proposed validator voting power must be > 0
//
// To remove:
// - proposed validator must be part of the set already
type PoA struct {
validators *avl.Tree // std.Address -> validators.Validator
}

// NewPoA creates a new empty Proof of Authority validator set
func NewPoA(opts ...Option) *PoA {
// Create the empty set
p := &PoA{
validators: avl.NewTree(),
}

// Apply the options
for _, opt := range opts {
opt(p)
}

return p
}

func (p *PoA) AddValidator(address std.Address, pubKey string, power uint64) (validators.Validator, error) {
// Validate that the operation is a valid call.
// Check if the validator is already in the set
if p.IsValidator(address) {
return validators.Validator{}, validators.ErrValidatorExists
}

// Make sure the voting power > 0
if power == 0 {
return validators.Validator{}, ErrInvalidVotingPower
}

v := validators.Validator{
Address: address,
PubKey: pubKey, // TODO: in the future, verify the public key
VotingPower: power,
}

// Add the validator to the set
p.validators.Set(address.String(), v)

return v, nil
}

func (p *PoA) RemoveValidator(address std.Address) (validators.Validator, error) {
// Validate that the operation is a valid call
// Fetch the validator
validator, err := p.GetValidator(address)
if err != nil {
return validators.Validator{}, err
}

// Remove the validator from the set
p.validators.Remove(address.String())

return validator, nil
}

func (p *PoA) IsValidator(address std.Address) bool {
_, exists := p.validators.Get(address.String())

return exists
}

func (p *PoA) GetValidator(address std.Address) (validators.Validator, error) {
validatorRaw, exists := p.validators.Get(address.String())
if !exists {
return validators.Validator{}, validators.ErrValidatorMissing
}

validator := validatorRaw.(validators.Validator)

return validator, nil
}

func (p *PoA) GetValidators() []validators.Validator {
vals := make([]validators.Validator, 0, p.validators.Size())

p.validators.Iterate("", "", func(_ string, value interface{}) bool {
validator := value.(validators.Validator)
vals = append(vals, validator)

return false
})

return vals
}
237 changes: 237 additions & 0 deletions examples/gno.land/p/nt/poa/poa_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package poa

import (
"testing"

"gno.land/p/demo/testutils"
"gno.land/p/demo/uassert"
"gno.land/p/demo/urequire"
"gno.land/p/sys/validators"

"gno.land/p/demo/ufmt"
)

// generateTestValidators generates a dummy validator set
func generateTestValidators(count int) []validators.Validator {
vals := make([]validators.Validator, 0, count)

for i := 0; i < count; i++ {
val := validators.Validator{
Address: testutils.TestAddress(ufmt.Sprintf("%d", i)),
PubKey: "public-key",
VotingPower: 1,
}

vals = append(vals, val)
}

return vals
}

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

t.Run("validator already in set", func(t *testing.T) {
t.Parallel()

var (
proposalAddress = testutils.TestAddress("caller")
proposalKey = "public-key"

initialSet = generateTestValidators(1)
)

initialSet[0].Address = proposalAddress
initialSet[0].PubKey = proposalKey

// Create the protocol with an initial set
p := NewPoA(WithInitialSet(initialSet))

// Attempt to add the validator
_, err := p.AddValidator(proposalAddress, proposalKey, 1)
uassert.ErrorIs(t, err, validators.ErrValidatorExists)
})

t.Run("invalid voting power", func(t *testing.T) {
t.Parallel()

var (
proposalAddress = testutils.TestAddress("caller")
proposalKey = "public-key"
)

// Create the protocol with no initial set
p := NewPoA()

// Attempt to add the validator
_, err := p.AddValidator(proposalAddress, proposalKey, 0)
uassert.ErrorIs(t, err, ErrInvalidVotingPower)
})
}

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

var (
proposalAddress = testutils.TestAddress("caller")
proposalKey = "public-key"
)

// Create the protocol with no initial set
p := NewPoA()

// Attempt to add the validator
_, err := p.AddValidator(proposalAddress, proposalKey, 1)
uassert.NoError(t, err)

// Make sure the validator is added
if !p.IsValidator(proposalAddress) || p.validators.Size() != 1 {
t.Fatal("address is not validator")
}
}

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

t.Run("proposed removal not in set", func(t *testing.T) {
t.Parallel()

var (
proposalAddress = testutils.TestAddress("caller")
initialSet = generateTestValidators(1)
)

initialSet[0].Address = proposalAddress

// Create the protocol with an initial set
p := NewPoA(WithInitialSet(initialSet))

// Attempt to remove the validator
_, err := p.RemoveValidator(testutils.TestAddress("totally random"))
uassert.ErrorIs(t, err, validators.ErrValidatorMissing)
})
}

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

var (
proposalAddress = testutils.TestAddress("caller")
initialSet = generateTestValidators(1)
)

initialSet[0].Address = proposalAddress

// Create the protocol with an initial set
p := NewPoA(WithInitialSet(initialSet))

// Attempt to remove the validator
_, err := p.RemoveValidator(proposalAddress)
urequire.NoError(t, err)

// Make sure the validator is removed
if p.IsValidator(proposalAddress) || p.validators.Size() != 0 {
t.Fatal("address is validator")
}
}

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

t.Run("validator not in set", func(t *testing.T) {
t.Parallel()

// Create the protocol with no initial set
p := NewPoA()

// Attempt to get the voting power
_, err := p.GetValidator(testutils.TestAddress("caller"))
uassert.ErrorIs(t, err, validators.ErrValidatorMissing)
})

t.Run("validator fetched", func(t *testing.T) {
t.Parallel()

var (
address = testutils.TestAddress("caller")
pubKey = "public-key"
votingPower = uint64(10)

initialSet = generateTestValidators(1)
)

initialSet[0].Address = address
initialSet[0].PubKey = pubKey
initialSet[0].VotingPower = votingPower

// Create the protocol with an initial set
p := NewPoA(WithInitialSet(initialSet))

// Get the validator
val, err := p.GetValidator(address)
urequire.NoError(t, err)

// Validate the address
if val.Address != address {
t.Fatal("invalid address")
}

// Validate the voting power
if val.VotingPower != votingPower {
t.Fatal("invalid voting power")
}

// Validate the public key
if val.PubKey != pubKey {
t.Fatal("invalid public key")
}
})
}

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

t.Run("empty set", func(t *testing.T) {
t.Parallel()

// Create the protocol with no initial set
p := NewPoA()

// Attempt to get the voting power
vals := p.GetValidators()

if len(vals) != 0 {
t.Fatal("validator set is not empty")
}
})

t.Run("validator set fetched", func(t *testing.T) {
t.Parallel()

initialSet := generateTestValidators(10)

// Create the protocol with an initial set
p := NewPoA(WithInitialSet(initialSet))

// Get the validator set
vals := p.GetValidators()

if len(vals) != len(initialSet) {
t.Fatal("returned validator set mismatch")
}

for _, val := range vals {
for _, initialVal := range initialSet {
if val.Address != initialVal.Address {
continue
}

// Validate the voting power
uassert.Equal(t, val.VotingPower, initialVal.VotingPower)

// Validate the public key
uassert.Equal(t, val.PubKey, initialVal.PubKey)
}
}
})
}
1 change: 1 addition & 0 deletions examples/gno.land/p/sys/validators/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/sys/validators
Loading

0 comments on commit f6ca518

Please sign in to comment.