-
Notifications
You must be signed in to change notification settings - Fork 375
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## 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
1 parent
608ca30
commit f6ca518
Showing
14 changed files
with
581 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/p/sys/validators |
Oops, something went wrong.