diff --git a/examples/gno.land/p/sys/vals/poc/gno.mod b/examples/gno.land/p/sys/vals/poc/gno.mod
new file mode 100644
index 00000000000..089aacab982
--- /dev/null
+++ b/examples/gno.land/p/sys/vals/poc/gno.mod
@@ -0,0 +1,8 @@
+module gno.land/p/sys/vals/poc
+
+require (
+ gno.land/p/demo/avl v0.0.0-latest
+ gno.land/p/demo/testutils v0.0.0-latest
+ gno.land/p/demo/ufmt v0.0.0-latest
+ gno.land/p/sys/vals/types v0.0.0-latest
+)
diff --git a/examples/gno.land/p/sys/vals/poc/option.gno b/examples/gno.land/p/sys/vals/poc/option.gno
new file mode 100644
index 00000000000..de01ac40b5d
--- /dev/null
+++ b/examples/gno.land/p/sys/vals/poc/option.gno
@@ -0,0 +1,15 @@
+package poc
+
+import "gno.land/p/sys/vals/types"
+
+type Option func(*PoC)
+
+// WithInitialSet sets the initial PoA validator set
+func WithInitialSet(validators []*types.Validator) Option {
+ return func(p *PoC) {
+ for index, validator := range validators {
+ p.validators = append(p.validators, validator)
+ p.addressToValidatorIndex.Set(validator.Address.String(), index)
+ }
+ }
+}
diff --git a/examples/gno.land/p/sys/vals/poc/poc.gno b/examples/gno.land/p/sys/vals/poc/poc.gno
new file mode 100644
index 00000000000..1fcb28492a1
--- /dev/null
+++ b/examples/gno.land/p/sys/vals/poc/poc.gno
@@ -0,0 +1,103 @@
+package poc
+
+import (
+ "std"
+
+ "gno.land/p/demo/avl"
+ "gno.land/p/sys/vals/types"
+)
+
+const errInvalidVotingPower = "invalid voting power"
+
+// PoC specifies the Proof of Contribution validator set.
+// In order to become part of the set, users to be voted into
+// the validator set by a govdao proposal
+type PoC struct {
+ // validators holds the current validator set.
+ // This slice can never practically grow more than ~150 elements,
+ // due to Tendermint's quadratic network complexity
+ validators []*types.Validator
+ addressToValidatorIndex *avl.Tree // address -> index
+}
+
+// NewPoC creates a new empty Proof of Contribution validator set
+func NewPoC(opts ...Option) *PoC {
+ // Create the empty set
+ p := &PoC{
+ validators: make([]*types.Validator, 0),
+ addressToValidatorIndex: avl.NewTree(),
+ }
+
+ // Apply the options
+ for _, opt := range opts {
+ opt(p)
+ }
+
+ return p
+}
+
+func (p *PoC) AddValidator(address std.Address, pubKey string, power uint64) *types.Validator {
+ // Validate that the operation is a valid call
+ // Check if the validator is already in the set
+ if p.IsValidator(address) {
+ panic(types.ErrValidatorExists)
+ }
+
+ // Make sure the voting power > 0
+ if power == 0 {
+ panic(errInvalidVotingPower)
+ }
+
+ v := &types.Validator{
+ Address: address,
+ PubKey: pubKey, // TODO: in the future, verify the public key
+ VotingPower: power,
+ }
+
+ // Add the validator to the set
+ p.addressToValidatorIndex.Set(v.Address.String(), len(p.validators))
+ p.validators = append(p.validators, v)
+
+ return v
+}
+
+func (p *PoC) RemoveValidator(address std.Address) *types.Validator {
+ // Validate that the operation is a valid call
+ // Check if the address is a validator
+ if !p.IsValidator(address) {
+ panic(types.ErrValidatorMissing)
+ }
+
+ addressStr := address.String()
+
+ // Fetch the validator index
+ indexRaw, _ := p.addressToValidatorIndex.Get(addressStr)
+ index := indexRaw.(int)
+
+ // Remove the validator from the set
+ validator := p.validators[index]
+ p.validators = append(p.validators[:index], p.validators[index+1:]...)
+
+ p.addressToValidatorIndex.Remove(addressStr)
+
+ return validator
+}
+
+func (p *PoC) IsValidator(address std.Address) bool {
+ _, exists := p.addressToValidatorIndex.Get(address.String())
+
+ return exists
+}
+
+func (p *PoC) GetValidator(address std.Address) types.Validator {
+ valIndexRaw, exists := p.addressToValidatorIndex.Get(address.String())
+ if !exists {
+ panic(types.ErrValidatorMissing)
+ }
+
+ return *p.validators[valIndexRaw.(int)]
+}
+
+func (p *PoC) GetValidators() []*types.Validator {
+ return p.validators
+}
diff --git a/examples/gno.land/p/sys/vals/poc/poc_test.gno b/examples/gno.land/p/sys/vals/poc/poc_test.gno
new file mode 100644
index 00000000000..e5799a0d462
--- /dev/null
+++ b/examples/gno.land/p/sys/vals/poc/poc_test.gno
@@ -0,0 +1,192 @@
+package poc
+
+import (
+ "testing"
+
+ "gno.land/p/demo/testutils"
+
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/sys/vals/types"
+)
+
+// generateTestValidators generates a dummy validator set
+func generateTestValidators(count int) []*types.Validator {
+ vals := make([]*types.Validator, 0, count)
+
+ for i := 0; i < count; i++ {
+ val := &types.Validator{
+ Address: testutils.TestAddress(ufmt.Sprintf("%d", i)),
+ PubKey: "public-key",
+ VotingPower: 1,
+ }
+
+ vals = append(vals, val)
+ }
+
+ return vals
+}
+
+func TestPoC_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 := NewPoC(WithInitialSet(initialSet))
+
+ // Attempt to add the validator
+ testing.PanicsWithError(t, types.ErrValidatorExists, func() {
+ p.AddValidator(proposalAddress, proposalKey, 1)
+ })
+ })
+
+ 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 := NewPoC()
+
+ // Attempt to add the validator
+ testing.PanicsWithError(t, errInvalidVotingPower, func() {
+ p.AddValidator(proposalAddress, proposalKey, 0)
+ })
+ })
+}
+
+func TestPoC_AddValidator(t *testing.T) {
+ t.Parallel()
+
+ var (
+ proposalAddress = testutils.TestAddress("caller")
+ proposalKey = "public-key"
+ )
+
+ // Create the protocol with no initial set
+ p := NewPoC()
+
+ // Attempt to add the validator
+ testing.NotPanics(t, func() {
+ p.AddValidator(proposalAddress, proposalKey, 1)
+ })
+
+ // Make sure the validator is added
+ if !p.IsValidator(proposalAddress) || len(p.validators) != 1 {
+ t.Fatal("address is not validator")
+ }
+}
+
+func TestPoC_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 := NewPoC(WithInitialSet(initialSet))
+
+ // Attempt to remove the validator
+ testing.PanicsWithError(t, types.ErrValidatorMissing, func() {
+ p.RemoveValidator(testutils.TestAddress("totally random"))
+ })
+ })
+}
+
+func TestPoC_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 := NewPoC(WithInitialSet(initialSet))
+
+ // Attempt to remove the validator
+ testing.NotPanics(t, func() {
+ p.RemoveValidator(proposalAddress)
+ })
+
+ // Make sure the validator is removed
+ if p.IsValidator(proposalAddress) || len(p.validators) != 0 {
+ t.Fatal("address is validator")
+ }
+}
+
+func TestPoC_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 := NewPoC()
+
+ // Attempt to get the voting power
+ testing.PanicsWithError(t, types.ErrValidatorMissing, func() {
+ p.GetValidator(testutils.TestAddress("caller"))
+ })
+ })
+
+ 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 := NewPoC(WithInitialSet(initialSet))
+
+ // Get the validator
+ val := p.GetValidator(address)
+
+ // 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")
+ }
+ })
+}
diff --git a/examples/gno.land/p/sys/vals/types/gno.mod b/examples/gno.land/p/sys/vals/types/gno.mod
new file mode 100644
index 00000000000..f728025168c
--- /dev/null
+++ b/examples/gno.land/p/sys/vals/types/gno.mod
@@ -0,0 +1 @@
+module gno.land/p/sys/vals/types
diff --git a/examples/gno.land/p/sys/vals/types/types.gno b/examples/gno.land/p/sys/vals/types/types.gno
new file mode 100644
index 00000000000..937d01b6e6f
--- /dev/null
+++ b/examples/gno.land/p/sys/vals/types/types.gno
@@ -0,0 +1,53 @@
+package types
+
+import (
+ "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
+
+ // 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
+
+ // 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
+
+ // 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
+)
+
+const (
+ // ErrCallerNotUserAccount is returned when the caller is not a user account
+ ErrCallerNotUserAccount = "caller not user account"
+
+ // ErrValidatorExists is returned when the validator is already in the set
+ ErrValidatorExists = "validator already exists"
+
+ // ErrValidatorMissing is returned when the validator is not in the set
+ ErrValidatorMissing = "validator doesn't exist"
+)
diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno
index 07ce461020b..8d41f62c9d3 100644
--- a/examples/gno.land/r/gnoland/home/home.gno
+++ b/examples/gno.land/r/gnoland/home/home.gno
@@ -182,7 +182,7 @@ func packageStaffPicks() ui.Element {
ui.BulletList{
ui.Link{URL: "r/sys/names"},
ui.Link{URL: "r/sys/rewards"},
- ui.Link{URL: "r/sys/validators"},
+ ui.Link{URL: "r/sys/vals"},
},
}, {
ui.H4("[r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)"),
diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno
index 919f8dd4fbc..4cb7664d14f 100644
--- a/examples/gno.land/r/gnoland/home/home_filetest.gno
+++ b/examples/gno.land/r/gnoland/home/home_filetest.gno
@@ -118,7 +118,7 @@ func main() {
//
// - [r/sys/names](r/sys/names)
// - [r/sys/rewards](r/sys/rewards)
-// - [r/sys/validators](r/sys/validators)
+// - [r/sys/vals](r/sys/vals)
//
//
//
diff --git a/examples/gno.land/r/gov/dao/prop1_filetest.gno b/examples/gno.land/r/gov/dao/prop1_filetest.gno
index 7072618a4a7..17472169dd0 100644
--- a/examples/gno.land/r/gov/dao/prop1_filetest.gno
+++ b/examples/gno.land/r/gov/dao/prop1_filetest.gno
@@ -10,24 +10,35 @@ package main
import (
"std"
+ "gno.land/p/sys/vals/types"
govdao "gno.land/r/gov/dao"
- "gno.land/r/sys/validators"
+ "gno.land/r/sys/vals"
)
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() []types.Validator {
+ return []types.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
+ },
}
}
// Wraps changesFn to emit a certified event only if executed from a
// complete governance proposal process.
- executor := validators.NewPropExecutor(changesFn)
+ executor := vals.NewPropExecutor(changesFn)
// Create a proposal.
// XXX: payment
@@ -45,13 +56,13 @@ func main() {
println("--")
println(govdao.Render("1"))
println("--")
- println(validators.Render(""))
+ println(vals.Render(""))
println("--")
govdao.ExecuteProposal(1)
println("--")
println(govdao.Render("1"))
println("--")
- println(validators.Render(""))
+ println(vals.Render(""))
}
// Output:
@@ -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/gno.mod b/examples/gno.land/r/sys/validators/gno.mod
deleted file mode 100644
index 51f6058a35a..00000000000
--- a/examples/gno.land/r/sys/validators/gno.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module gno.land/r/sys/validators
-
-require gno.land/p/gov/proposal v0.0.0-latest
diff --git a/examples/gno.land/r/sys/validators/validators.gno b/examples/gno.land/r/sys/validators/validators.gno
deleted file mode 100644
index 669b688727a..00000000000
--- a/examples/gno.land/r/sys/validators/validators.gno
+++ /dev/null
@@ -1,60 +0,0 @@
-// Package validators is used to manage the validator set.
-package validators
-
-import (
- "std"
- "strconv"
-
- "gno.land/p/gov/proposal"
-)
-
-var unappliedChanges = []Change{}
-
-// Change represents a change in the validator set.
-type Change struct {
- Address std.Address
- Power int
-}
-
-// 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")
- }
-
- // Certify that the changes are sent from the context of this realm.
- callback := func() error {
- newChanges := changesFn()
-
- // emit for external clients
- std.Emit("newChanges") // XXX: pass parameters
-
- // append to slice for gno.land
- unappliedChanges = append(unappliedChanges, newChanges...)
- return nil
- }
-
- exec := proposal.NewExecutor(callback)
- return exec
-}
-
-// this function is unexported and intended to be called by the chain.
-func getAndResetChanges() []Change {
- cpy := unappliedChanges[:]
- unappliedChanges = []Change{}
- return cpy
-}
-
-func Render(_ string) string {
- if len(unappliedChanges) == 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"
- }
- return output
-}
diff --git a/examples/gno.land/r/sys/vals/doc.gno b/examples/gno.land/r/sys/vals/doc.gno
new file mode 100644
index 00000000000..75405923376
--- /dev/null
+++ b/examples/gno.land/r/sys/vals/doc.gno
@@ -0,0 +1,3 @@
+// Package vals 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 vals
diff --git a/examples/gno.land/r/sys/vals/gno.mod b/examples/gno.land/r/sys/vals/gno.mod
new file mode 100644
index 00000000000..221b2e3370c
--- /dev/null
+++ b/examples/gno.land/r/sys/vals/gno.mod
@@ -0,0 +1,7 @@
+module gno.land/r/sys/vals
+
+require (
+ gno.land/p/gov/proposal v0.0.0-latest
+ gno.land/p/sys/vals/poc v0.0.0-latest
+ gno.land/p/sys/vals/types v0.0.0-latest
+)
diff --git a/examples/gno.land/r/sys/vals/init.gno b/examples/gno.land/r/sys/vals/init.gno
new file mode 100644
index 00000000000..5da5ad3d736
--- /dev/null
+++ b/examples/gno.land/r/sys/vals/init.gno
@@ -0,0 +1,13 @@
+package vals
+
+import (
+ "gno.land/p/sys/vals/poc"
+ "gno.land/p/sys/vals/types"
+)
+
+func init() {
+ v = &vals{
+ p: poc.NewPoC(),
+ changes: make([]types.Validator, 0),
+ }
+}
diff --git a/examples/gno.land/r/sys/vals/poc.gno b/examples/gno.land/r/sys/vals/poc.gno
new file mode 100644
index 00000000000..fafd539ddea
--- /dev/null
+++ b/examples/gno.land/r/sys/vals/poc.gno
@@ -0,0 +1,66 @@
+package vals
+
+import (
+ "std"
+
+ "gno.land/p/gov/proposal"
+ "gno.land/p/sys/vals/types"
+)
+
+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() []types.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(address string) bool {
+ return v.p.IsValidator(std.Address(address))
+}
+
+// GetValidators returns the typed validator set
+func GetValidators() []*types.Validator {
+ return v.p.GetValidators()
+}
diff --git a/examples/gno.land/r/sys/vals/vals.gno b/examples/gno.land/r/sys/vals/vals.gno
new file mode 100644
index 00000000000..72ec5bad5f5
--- /dev/null
+++ b/examples/gno.land/r/sys/vals/vals.gno
@@ -0,0 +1,73 @@
+package vals
+
+import (
+ "std"
+ "strconv"
+
+ "gno.land/p/sys/vals/types"
+)
+
+// vals is the wrapper for the validator set protocol
+type vals struct {
+ p types.ValsetProtocol // p is the underlying validator set protocol
+ changes []types.Validator // changes are the set changes that happened between scrapes
+}
+
+// v holds the active on-chain validator set state
+var v *vals
+
+// addValidator adds a new validator to the validator set.
+// If the validator is already present, the method errors out
+func addValidator(validator types.Validator) {
+ if val := v.p.AddValidator(
+ validator.Address,
+ validator.PubKey,
+ validator.VotingPower,
+ ); val != nil {
+ // Validator added, note the change
+ v.changes = append(v.changes, *val)
+
+ // Emit the validator set change
+ std.Emit(types.ValidatorAddedEvent)
+ }
+}
+
+// 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) {
+ if val := v.p.RemoveValidator(address); val != nil {
+ // Validator removed, note the change
+ v.changes = append(v.changes, types.Validator{
+ Address: val.Address,
+ PubKey: val.PubKey,
+ VotingPower: 0, // nullified the voting power indicates removal
+ })
+
+ // Emit the validator set change
+ std.Emit(types.ValidatorRemovedEvent)
+ }
+}
+
+// getChanges returns the validator changes stored on the realm
+func getChanges() []types.Validator {
+ // Construct the changes
+ changes := make([]types.Validator, len(v.changes))
+ copy(changes, v.changes)
+
+ // Reset the changes set
+ v.changes = v.changes[:0]
+
+ return changes
+}
+
+func Render(_ string) string {
+ if len(v.changes) == 0 {
+ return "No valset changes to apply."
+ }
+
+ output := "Valset changes to apply:\n"
+ for _, change := range v.changes {
+ output += "- " + string(change.Address) + " (" + strconv.FormatUint(change.VotingPower, 10) + ")\n"
+ }
+ return output
+}
diff --git a/gno.land/cmd/gnoland/genesis_validator.go b/gno.land/cmd/gnoland/genesis_validator.go
index 91d3e4af7dd..f09733323f6 100644
--- a/gno.land/cmd/gnoland/genesis_validator.go
+++ b/gno.land/cmd/gnoland/genesis_validator.go
@@ -1,11 +1,25 @@
package main
import (
+ "errors"
"flag"
+ "fmt"
+ "path/filepath"
+ "github.com/gnolang/gno/gno.land/pkg/gnoland"
+ "github.com/gnolang/gno/gno.land/pkg/sdk/vm"
+ "github.com/gnolang/gno/gno.land/pkg/valset"
+ "github.com/gnolang/gno/tm2/pkg/bft/config"
+ "github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/gno/tm2/pkg/commands"
+ osm "github.com/gnolang/gno/tm2/pkg/os"
+ "github.com/gnolang/gno/tm2/pkg/std"
)
+const valsRealm = "gno.land/r/sys/vals"
+
+var errMissingSysPoCDeployment = errors.New("missing r/sys/vals deployment for PoC")
+
type validatorCfg struct {
commonCfg
@@ -47,3 +61,77 @@ func (c *validatorCfg) RegisterFlags(fs *flag.FlagSet) {
"the gno bech32 address of the validator",
)
}
+
+// alignChainValset aligns the validator set in the genesis state with any on-chain valset protocol
+func alignChainValset(genesisPath string, genesis *types.GenesisDoc) error {
+ // Construct the config path
+ var (
+ nodeDir = filepath.Join(filepath.Dir(genesisPath), defaultNodeDir)
+ configPath = constructConfigPath(nodeDir)
+
+ cfg = config.DefaultConfig()
+ err error
+ )
+
+ // Check if there is an existing config file
+ if osm.FileExists(configPath) {
+ // Attempt to grab the config from disk
+ cfg, err = config.LoadConfig(nodeDir)
+ if err != nil {
+ return fmt.Errorf("unable to load config file, %w", err)
+ }
+ }
+
+ state := genesis.AppState.(gnoland.GnoGenesisState)
+
+ switch cfg.ValsetProtocol {
+ case config.ProofOfContribution:
+ // Find the /r/sys/vals deploy transaction
+ pkg := findSysValsDeployment(state.Txs)
+ if pkg == nil {
+ return errMissingSysPoCDeployment
+ }
+
+ // Modify the deploy transaction to include the current
+ // genesis.json validator set
+ if err := valset.ModifyPoCDeployment(pkg, genesis.Validators); err != nil {
+ return fmt.Errorf("unable to modify PoC deployment, %w", err)
+ }
+
+ // Update the app state
+ genesis.AppState = state
+ default:
+ // No on-chain valset protocol
+ return nil
+ }
+
+ return nil
+}
+
+// findSysValsDeployment finds the package deployment for `r/sys/vals`,
+// among the given transaction list. Returns nil if no deployment was found
+func findSysValsDeployment(txs []std.Tx) *std.MemPackage {
+ addPkgType := vm.MsgAddPackage{}.Type()
+
+ for _, tx := range txs {
+ for _, msg := range tx.Msgs {
+ // Make sure the transaction is a deploy-tx
+ if msg.Type() != addPkgType {
+ continue
+ }
+
+ // Cast the message
+ addPkg := msg.(vm.MsgAddPackage)
+
+ // Check if the message is a Realm
+ // deployment for r/sys/vals
+ if addPkg.Package.Path != valsRealm {
+ continue
+ }
+
+ return addPkg.Package
+ }
+ }
+
+ return nil
+}
diff --git a/gno.land/cmd/gnoland/genesis_validator_add.go b/gno.land/cmd/gnoland/genesis_validator_add.go
index 6c44ad93f89..315c62c8330 100644
--- a/gno.land/cmd/gnoland/genesis_validator_add.go
+++ b/gno.land/cmd/gnoland/genesis_validator_add.go
@@ -123,8 +123,13 @@ func execValidatorAdd(cfg *validatorAddCfg, io commands.IO) error {
// Add the validator
genesis.Validators = append(genesis.Validators, validator)
+ // Update the on-chain validator set, if any
+ if err = alignChainValset(cfg.rootCfg.genesisPath, genesis); err != nil {
+ return fmt.Errorf("unable to align on-chain valset, %w", err)
+ }
+
// Save the updated genesis
- if err := genesis.SaveAs(cfg.rootCfg.genesisPath); err != nil {
+ if err = genesis.SaveAs(cfg.rootCfg.genesisPath); err != nil {
return fmt.Errorf("unable to save genesis.json, %w", err)
}
diff --git a/gno.land/cmd/gnoland/genesis_validator_remove.go b/gno.land/cmd/gnoland/genesis_validator_remove.go
index 48a15a9abaf..9b20997fd27 100644
--- a/gno.land/cmd/gnoland/genesis_validator_remove.go
+++ b/gno.land/cmd/gnoland/genesis_validator_remove.go
@@ -57,6 +57,11 @@ func execValidatorRemove(cfg *validatorCfg, io commands.IO) error {
// Drop the validator
genesis.Validators = append(genesis.Validators[:index], genesis.Validators[index+1:]...)
+ // Update the on-chain validator set, if any
+ if err = alignChainValset(cfg.genesisPath, genesis); err != nil {
+ return fmt.Errorf("unable to align on-chain valset, %w", err)
+ }
+
// Save the updated genesis
if err := genesis.SaveAs(cfg.genesisPath); err != nil {
return fmt.Errorf("unable to save genesis.json, %w", err)
diff --git a/gno.land/pkg/valset/template.go b/gno.land/pkg/valset/template.go
new file mode 100644
index 00000000000..9cfebf81494
--- /dev/null
+++ b/gno.land/pkg/valset/template.go
@@ -0,0 +1,49 @@
+package valset
+
+import (
+ "fmt"
+ "strings"
+ "text/template"
+
+ "github.com/gnolang/gno/tm2/pkg/bft/types"
+)
+
+// validator defines the Go version of Gno's:
+// gno.land/p/sys/vals/types Validator
+type validator struct {
+ Address string // bech32 address
+ PubKey string // bech32 representation of the public key
+ VotingPower uint64
+}
+
+// generateInitBody generates the templated initialization
+// body for the gno.land/r/sys/vals Realm
+func generateInitBody(
+ set []types.GenesisValidator,
+ setTemplate string,
+) (string, error) {
+ // Parse the template
+ tmpl, err := template.New("init-realm").Parse(setTemplate)
+ if err != nil {
+ return "", fmt.Errorf("unable to parse template, %w", err)
+ }
+
+ vals := make([]validator, 0, len(set))
+
+ for _, v := range set {
+ vals = append(vals, validator{
+ Address: v.Address.String(),
+ PubKey: v.PubKey.String(),
+ VotingPower: uint64(v.Power),
+ })
+ }
+
+ // Apply the valset to the template
+ var builder strings.Builder
+
+ if err = tmpl.Execute(&builder, vals); err != nil {
+ return "", fmt.Errorf("unable to execute template, %w", err)
+ }
+
+ return builder.String(), nil
+}
diff --git a/gno.land/pkg/valset/valset.go b/gno.land/pkg/valset/valset.go
new file mode 100644
index 00000000000..1b1de29ce9c
--- /dev/null
+++ b/gno.land/pkg/valset/valset.go
@@ -0,0 +1,78 @@
+package valset
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/gnolang/gno/tm2/pkg/bft/types"
+ "github.com/gnolang/gno/tm2/pkg/std"
+)
+
+const initFile = "init.gno"
+
+var errMissingInit = errors.New("missing init.gno")
+
+// ModifyPoCDeployment modifies the initialization Realm
+// for Proof of Contribution to be aligned with the given set
+func ModifyPoCDeployment(
+ file *std.MemPackage,
+ set []types.GenesisValidator,
+) error {
+ return modifyValsetDeployment(file, set, pocTemplate)
+}
+
+// modifyValsetDeployment modifies the generic initialization Realm
+// for a valset protocol to be aligned with the given set. Utilizes
+// the given template to append the initial validator set
+func modifyValsetDeployment(
+ file *std.MemPackage,
+ set []types.GenesisValidator,
+ setTemplate string,
+) error {
+ // Find the `init.gno` file
+ for _, f := range file.Files {
+ if f.Name != initFile {
+ continue
+ }
+
+ // Generate the Realm `init.gno` body
+ body, err := generateInitBody(set, setTemplate)
+ if err != nil {
+ return fmt.Errorf("unable to prepare valset init body, %w", err)
+ }
+
+ f.Body = body
+
+ return nil
+ }
+
+ return errMissingInit
+}
+
+const pocTemplate = `
+package vals
+
+import (
+ "std"
+
+ "gno.land/p/sys/vals/poc"
+ "gno.land/p/sys/vals/types"
+)
+
+func init() {
+ set := []*types.Validator{
+ {{ range . }}
+ {
+ Address: std.Address("{{ .Address }}"),
+ PubKey: "{{ .PubKey }}",
+ VotingPower: {{ .VotingPower }},
+ },
+ {{ end }}
+ }
+
+ v = &vals{
+ p: poc.NewPoC(WithInitialSet(set)),
+ changes: make([]types.Validator, 0),
+ }
+}
+`
diff --git a/gnovm/stdlibs/testing/testing.gno b/gnovm/stdlibs/testing/testing.gno
index 7192a2677d9..a5ae7be05ad 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
@@ -39,6 +39,67 @@ func Recover(result Setter) {
panic(r)
}
+func extractPanicErr(r interface{}) string {
+ err, ok := r.(error)
+ if ok {
+ return err.Error()
+ }
+
+ errStr, ok := r.(string)
+ if ok {
+ return errStr
+ }
+
+ return "unknown error"
+}
+
+// PanicsWithError asserts that the code inside the specified func panics,
+// and that the recovered panic value is an error that satisfies the given message
+func PanicsWithError(t *T, errString string, f func()) bool {
+ t.Helper()
+
+ var valid bool
+
+ defer func() {
+ if r := recover(); r != nil {
+ // Check if the error matches
+ panicErr := extractPanicErr(r)
+ if panicErr == errString {
+ valid = true
+
+ return
+ }
+
+ t.Fatalf("Function panicked with err, %s", panicErr)
+ }
+ }()
+
+ // Run the callback
+ f()
+
+ return valid
+}
+
+// NotPanics asserts that the code inside the specified func does NOT panic
+func NotPanics(t *T, f func()) bool {
+ t.Helper()
+
+ valid := true
+
+ defer func() {
+ if r := recover(); r != nil {
+ valid = false
+
+ t.Fatalf("Function panicked with err, %s", extractPanicErr(r))
+ }
+ }()
+
+ // Run the callback
+ f()
+
+ return valid
+}
+
type Setter interface {
Set(v interface{})
}
@@ -60,7 +121,7 @@ func AllocsPerRun2(runs int, f func()) (total int) {
return 0
}
-//----------------------------------------
+// ----------------------------------------
// T
type T struct {
@@ -225,7 +286,7 @@ func (t *T) report() Report {
}
}
-//----------------------------------------
+// ----------------------------------------
// B
// TODO: actually implement
@@ -261,7 +322,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
diff --git a/tm2/pkg/bft/config/config.go b/tm2/pkg/bft/config/config.go
index f9e9a0cd899..71e27d746a9 100644
--- a/tm2/pkg/bft/config/config.go
+++ b/tm2/pkg/bft/config/config.go
@@ -37,6 +37,8 @@ const (
SocketABCI = "socket"
)
+const ProofOfContribution = "poc"
+
// Regular expression for TCP or UNIX socket address
// TCP address: host:port (IPv4 example)
// UNIX address: unix:// followed by the path
@@ -322,6 +324,9 @@ type BaseConfig struct {
// If true, query the ABCI app on connecting to a new peer
// so the app can decide if we should keep the connection or not
FilterPeers bool `toml:"filter_peers" comment:"If true, query the ABCI app on connecting to a new peer\n so the app can decide if we should keep the connection or not"` // false
+
+ // The validator set management protocol, ex. PoC / PoA / PoS
+ ValsetProtocol string `toml:"valset_protocol" comment:"The validator set management protocol to utilize"`
}
// DefaultBaseConfig returns a default base configuration for a Tendermint node
@@ -338,6 +343,7 @@ func DefaultBaseConfig() BaseConfig {
FilterPeers: false,
DBBackend: db.GoLevelDBBackend.String(),
DBPath: DefaultDBDir,
+ ValsetProtocol: "", // none by default
}
}