diff --git a/examples/gno.land/p/nt/poa/gno.mod b/examples/gno.land/p/nt/poa/gno.mod new file mode 100644 index 00000000000..5c1b75eb05a --- /dev/null +++ b/examples/gno.land/p/nt/poa/gno.mod @@ -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 +) diff --git a/examples/gno.land/p/nt/poa/option.gno b/examples/gno.land/p/nt/poa/option.gno new file mode 100644 index 00000000000..051ab2611f1 --- /dev/null +++ b/examples/gno.land/p/nt/poa/option.gno @@ -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) + } + } +} diff --git a/examples/gno.land/p/nt/poa/poa.gno b/examples/gno.land/p/nt/poa/poa.gno new file mode 100644 index 00000000000..1eab427f642 --- /dev/null +++ b/examples/gno.land/p/nt/poa/poa.gno @@ -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 +} diff --git a/examples/gno.land/p/nt/poa/poa_test.gno b/examples/gno.land/p/nt/poa/poa_test.gno new file mode 100644 index 00000000000..dfec387774c --- /dev/null +++ b/examples/gno.land/p/nt/poa/poa_test.gno @@ -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) + } + } + }) +} diff --git a/examples/gno.land/p/sys/validators/gno.mod b/examples/gno.land/p/sys/validators/gno.mod new file mode 100644 index 00000000000..9c7a38aada0 --- /dev/null +++ b/examples/gno.land/p/sys/validators/gno.mod @@ -0,0 +1 @@ +module gno.land/p/sys/validators diff --git a/examples/gno.land/p/sys/validators/types.gno b/examples/gno.land/p/sys/validators/types.gno new file mode 100644 index 00000000000..bd7d5df2ba8 --- /dev/null +++ b/examples/gno.land/p/sys/validators/types.gno @@ -0,0 +1,51 @@ +package validators + +import ( + "errors" + "std" +) + +// ValsetProtocol defines the validator set protocol (PoA / PoS / PoC / ?) +type ValsetProtocol interface { + // AddValidator adds a new validator to the validator set. + // If the validator is already present, the method should error out + // + // TODO: This API is not ideal -- the address should be derived from + // the public key, and not be passed in as such, but currently Gno + // does not support crypto address derivation + AddValidator(address std.Address, pubKey string, power uint64) (Validator, error) + + // RemoveValidator removes the given validator from the set. + // If the validator is not present in the set, the method should error out + RemoveValidator(address std.Address) (Validator, error) + + // IsValidator returns a flag indicating if the given + // bech32 address is part of the validator set + IsValidator(address std.Address) bool + + // GetValidator returns the validator using the given address + GetValidator(address std.Address) (Validator, error) + + // GetValidators returns the currently active validator set + GetValidators() []Validator +} + +// Validator represents a single chain validator +type Validator struct { + Address std.Address // bech32 address + PubKey string // bech32 representation of the public key + VotingPower uint64 +} + +const ( + ValidatorAddedEvent = "ValidatorAdded" // emitted when a validator was added to the set + ValidatorRemovedEvent = "ValidatorRemoved" // emitted when a validator was removed from the set +) + +var ( + // ErrValidatorExists is returned when the validator is already in the set + ErrValidatorExists = errors.New("validator already exists") + + // ErrValidatorMissing is returned when the validator is not in the set + ErrValidatorMissing = errors.New("validator doesn't exist") +) diff --git a/examples/gno.land/r/gov/dao/prop1_filetest.gno b/examples/gno.land/r/gov/dao/prop1_filetest.gno index 7072618a4a7..1409a51960f 100644 --- a/examples/gno.land/r/gov/dao/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/prop1_filetest.gno @@ -10,18 +10,29 @@ package main import ( "std" + pVals "gno.land/p/sys/validators" govdao "gno.land/r/gov/dao" "gno.land/r/sys/validators" ) func init() { - // Create the validators change proposal. - changesFn := func() []validators.Change { - return []validators.Change{ - // add a new validator. - {Address: std.Address("g12345678"), Power: 1}, - // remove an existing validator. - {Address: std.Address("g000000000"), Power: 0}, + changesFn := func() []pVals.Validator { + return []pVals.Validator{ + { + Address: std.Address("g12345678"), + PubKey: "pubkey", + VotingPower: 10, // add a new validator + }, + { + Address: std.Address("g000000000"), + PubKey: "pubkey", + VotingPower: 10, // add a new validator + }, + { + Address: std.Address("g000000000"), + PubKey: "pubkey", + VotingPower: 0, // remove an existing validator + }, } } @@ -81,5 +92,6 @@ func main() { // Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm // -- // Valset changes to apply: -// - g12345678 (1) +// - g12345678 (10) +// - g000000000 (10) // - g000000000 (0) diff --git a/examples/gno.land/r/sys/validators/doc.gno b/examples/gno.land/r/sys/validators/doc.gno new file mode 100644 index 00000000000..d17b29ed110 --- /dev/null +++ b/examples/gno.land/r/sys/validators/doc.gno @@ -0,0 +1,3 @@ +// Package validators implements the on-chain validator set management through Proof of Contribution. +// The Realm exposes only a public executor for govdao proposals, that can suggest validator set changes. +package validators diff --git a/examples/gno.land/r/sys/validators/gno.mod b/examples/gno.land/r/sys/validators/gno.mod index 51f6058a35a..7025baa7a2a 100644 --- a/examples/gno.land/r/sys/validators/gno.mod +++ b/examples/gno.land/r/sys/validators/gno.mod @@ -1,3 +1,7 @@ module gno.land/r/sys/validators -require gno.land/p/gov/proposal v0.0.0-latest +require ( + gno.land/p/gov/proposal v0.0.0-latest + gno.land/p/nt/poa v0.0.0-latest + gno.land/p/sys/validators v0.0.0-latest +) diff --git a/examples/gno.land/r/sys/validators/gnosdk.gno b/examples/gno.land/r/sys/validators/gnosdk.gno new file mode 100644 index 00000000000..ae369726e47 --- /dev/null +++ b/examples/gno.land/r/sys/validators/gnosdk.gno @@ -0,0 +1,16 @@ +package validators + +import "gno.land/p/sys/validators" + +// getChanges returns the validator changes stored on the realm. +// This function is unexported and intended to be called by gno.land through the GnoSDK +func getChanges() []validators.Validator { + // Construct the changes + valsetChanges := make([]validators.Validator, len(changes)) + copy(valsetChanges, changes) + + // Reset the changes set + changes = changes[:0] + + return valsetChanges +} diff --git a/examples/gno.land/r/sys/validators/init.gno b/examples/gno.land/r/sys/validators/init.gno new file mode 100644 index 00000000000..72a0cda87cc --- /dev/null +++ b/examples/gno.land/r/sys/validators/init.gno @@ -0,0 +1,14 @@ +package validators + +import ( + "gno.land/p/nt/poa" + "gno.land/p/sys/validators" +) + +func init() { + // The default valset protocol is PoA + vp = poa.NewPoA() + + // No changes to apply initially + changes = make([]validators.Validator, 0) +} diff --git a/examples/gno.land/r/sys/validators/poc.gno b/examples/gno.land/r/sys/validators/poc.gno new file mode 100644 index 00000000000..e088b3b4293 --- /dev/null +++ b/examples/gno.land/r/sys/validators/poc.gno @@ -0,0 +1,66 @@ +package validators + +import ( + "std" + + "gno.land/p/gov/proposal" + "gno.land/p/sys/validators" +) + +const daoPkgPath = "gno.land/r/gov/dao" + +const ( + errNoChangesProposed = "no set changes proposed" + errNotGovDAO = "caller not govdao executor" +) + +// NewPropExecutor creates a new executor that wraps a changes closure +// proposal. This wrapper is required to ensure the GovDAO Realm actually +// executed the callback. +// +// Concept adapted from: +// https://github.com/gnolang/gno/pull/1945 +func NewPropExecutor(changesFn func() []validators.Validator) proposal.Executor { + if changesFn == nil { + panic(errNoChangesProposed) + } + + callback := func() error { + // Make sure the GovDAO executor runs the valset changes + assertGovDAOCaller() + + for _, change := range changesFn() { + if change.VotingPower == 0 { + // This change request is to remove the validator + removeValidator(change.Address) + + continue + } + + // This change request is to add the validator + addValidator(change) + } + + return nil + } + + return proposal.NewExecutor(callback) +} + +// assertGovDAOCaller verifies the caller is the GovDAO executor +func assertGovDAOCaller() { + if std.PrevRealm().PkgPath() != daoPkgPath { + panic(errNotGovDAO) + } +} + +// IsValidator returns a flag indicating if the given bech32 address +// is part of the validator set +func IsValidator(addr std.Address) bool { + return vp.IsValidator(addr) +} + +// GetValidators returns the typed validator set +func GetValidators() []validators.Validator { + return vp.GetValidators() +} diff --git a/examples/gno.land/r/sys/validators/validators.gno b/examples/gno.land/r/sys/validators/validators.gno index 669b688727a..c40b550e6dc 100644 --- a/examples/gno.land/r/sys/validators/validators.gno +++ b/examples/gno.land/r/sys/validators/validators.gno @@ -1,60 +1,60 @@ -// Package validators is used to manage the validator set. package validators import ( "std" "strconv" - "gno.land/p/gov/proposal" + "gno.land/p/sys/validators" ) -var unappliedChanges = []Change{} - -// Change represents a change in the validator set. -type Change struct { - Address std.Address - Power int -} +var ( + vp validators.ValsetProtocol // p is the underlying validator set protocol + changes []validators.Validator // changes are the set changes that happened between scrapes +) -// NewPropExecutor creates a new executor that wraps a changes closure -// proposal. It emits a typed object (subscribed by tm2) only if it passes -// through a complete p/gov/proposal process. -func NewPropExecutor(changesFn func() []Change) proposal.Executor { - if changesFn == nil { - panic("changesFn should not be nil") +// addValidator adds a new validator to the validator set. +// If the validator is already present, the method errors out +func addValidator(validator validators.Validator) { + val, err := vp.AddValidator(validator.Address, validator.PubKey, validator.VotingPower) + if err != nil { + panic(err) } - // Certify that the changes are sent from the context of this realm. - callback := func() error { - newChanges := changesFn() + // Validator added, note the change + changes = append(changes, val) - // emit for external clients - std.Emit("newChanges") // XXX: pass parameters + // Emit the validator set change + std.Emit(validators.ValidatorAddedEvent) +} - // append to slice for gno.land - unappliedChanges = append(unappliedChanges, newChanges...) - return nil +// removeValidator removes the given validator from the set. +// If the validator is not present in the set, the method errors out +func removeValidator(address std.Address) { + val, err := vp.RemoveValidator(address) + if err != nil { + panic(err) } - exec := proposal.NewExecutor(callback) - return exec -} + // Validator removed, note the change + changes = append(changes, validators.Validator{ + Address: val.Address, + PubKey: val.PubKey, + VotingPower: 0, // nullified the voting power indicates removal + }) -// this function is unexported and intended to be called by the chain. -func getAndResetChanges() []Change { - cpy := unappliedChanges[:] - unappliedChanges = []Change{} - return cpy + // Emit the validator set change + std.Emit(validators.ValidatorRemovedEvent) } func Render(_ string) string { - if len(unappliedChanges) == 0 { + if len(changes) == 0 { return "No valset changes to apply." } output := "Valset changes to apply:\n" - for _, change := range unappliedChanges { - output += "- " + string(change.Address) + " (" + strconv.Itoa(change.Power) + ")\n" + for _, change := range changes { + output += "- " + string(change.Address) + " (" + strconv.FormatUint(change.VotingPower, 10) + ")\n" } + return output } diff --git a/gnovm/stdlibs/testing/testing.gno b/gnovm/stdlibs/testing/testing.gno index 7192a2677d9..6e55c5cc283 100644 --- a/gnovm/stdlibs/testing/testing.gno +++ b/gnovm/stdlibs/testing/testing.gno @@ -9,7 +9,7 @@ import ( "strings" ) -//---------------------------------------- +// ---------------------------------------- // Top level functions // skipErr is the type of the panic created by SkipNow @@ -60,7 +60,7 @@ func AllocsPerRun2(runs int, f func()) (total int) { return 0 } -//---------------------------------------- +// ---------------------------------------- // T type T struct { @@ -225,7 +225,7 @@ func (t *T) report() Report { } } -//---------------------------------------- +// ---------------------------------------- // B // TODO: actually implement @@ -261,7 +261,7 @@ func (b *B) StartTimer() { panic("not yet implemen func (b *B) StopTimer() { panic("not yet implemented") } func (b *B) TempDir() string { panic("not yet implemented") } -//---------------------------------------- +// ---------------------------------------- // PB // TODO: actually implement