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 } }