diff --git a/.pending/features/sdk/4409-migration-scrip b/.pending/features/sdk/4409-migration-scrip new file mode 100644 index 000000000000..2f259589dee0 --- /dev/null +++ b/.pending/features/sdk/4409-migration-scrip @@ -0,0 +1,3 @@ +#4409 Implement a command that migrates exported state from one version to the next. +The `migrate` command currently supports migrating from v0.34 to v0.36 by implementing +necessary types for both versions. diff --git a/contrib/export/lib.py b/contrib/migrate/lib.py similarity index 100% rename from contrib/export/lib.py rename to contrib/migrate/lib.py diff --git a/contrib/export/v0.33.x-to-v0.34.0.py b/contrib/migrate/v0.33.x-to-v0.34.0.py similarity index 92% rename from contrib/export/v0.33.x-to-v0.34.0.py rename to contrib/migrate/v0.33.x-to-v0.34.0.py index fd49e9b697ff..8ca515a8a856 100755 --- a/contrib/export/v0.33.x-to-v0.34.0.py +++ b/contrib/migrate/v0.33.x-to-v0.34.0.py @@ -5,8 +5,9 @@ def process_raw_genesis(genesis, parsed_args): # update genesis with breaking changes - genesis['consensus_params']['block'] = genesis['consensus_params']['block_size'] - del genesis['consensus_params']['block_size'] + if 'block_size' in genesis['consensus_params']: + genesis['consensus_params']['block'] = genesis['consensus_params']['block_size'] + del genesis['consensus_params']['block_size'] genesis['app_state']['crisis'] = { 'constant_fee': { diff --git a/docs/clients/README.md b/docs/clients/README.md index 835b6ee5095b..ee972577c121 100644 --- a/docs/clients/README.md +++ b/docs/clients/README.md @@ -16,3 +16,11 @@ Light-clients enable users to interact with your application without having to d - [Command-Line interface for SDK-based blockchain](./cli.md) - [Service provider doc](./service-providers.md) + +## Genesis upgrade + +If you need to upgrade your node you could export the genesis and migrate it to the new version through this script: + +```bash + migrate v0.36 genesis_0_34.json [--time "2019-04-22T17:00:11Z"] [--chain-id test] > ~/.gaiad/genesis.json +``` diff --git a/x/genutil/client/cli/migrate.go b/x/genutil/client/cli/migrate.go new file mode 100644 index 000000000000..74d9f9fb3fb0 --- /dev/null +++ b/x/genutil/client/cli/migrate.go @@ -0,0 +1,87 @@ +package cli + +import ( + "fmt" + "time" + + "github.com/spf13/cobra" + "github.com/tendermint/tendermint/types" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/version" + extypes "github.com/cosmos/cosmos-sdk/x/genutil" + "github.com/cosmos/cosmos-sdk/x/genutil/legacy/v036" +) + +var migrationMap = extypes.MigrationMap{ + "v0.36": v036.Migrate, +} + +const ( + flagGenesisTime = "genesis-time" + flagChainId = "chain-id" +) + +func MigrateGenesisCmd(_ *server.Context, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "migrate [target-version] [genesis-file]", + Short: "Migrate genesis to a specified target version", + Long: fmt.Sprintf(`Migrate the source genesis into the target version and print to STDOUT. + +Example: +$ %s migrate v0.36 /path/to/genesis.json --chain-id=cosmoshub-3 --genesis-time=2019-04-22T17:00:00Z +`, version.ServerName), + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + target := args[0] + importGenesis := args[1] + + genDoc, err := types.GenesisDocFromFile(importGenesis) + if err != nil { + return err + } + + var initialState extypes.AppMap + cdc.MustUnmarshalJSON(genDoc.AppState, &initialState) + + if migrationMap[target] == nil { + return fmt.Errorf("unknown migration function version: %s", target) + } + + newGenState := migrationMap[target](initialState, cdc) + genDoc.AppState = cdc.MustMarshalJSON(newGenState) + + genesisTime := cmd.Flag(flagGenesisTime).Value.String() + if genesisTime != "" { + var t time.Time + + err := t.UnmarshalText([]byte(genesisTime)) + if err != nil { + return err + } + + genDoc.GenesisTime = t + } + + chainId := cmd.Flag(flagChainId).Value.String() + if chainId != "" { + genDoc.ChainID = chainId + } + + out, err := cdc.MarshalJSONIndent(genDoc, "", " ") + if err != nil { + return err + } + + fmt.Println(string(out)) + return nil + + }, + } + + cmd.Flags().String(flagGenesisTime, "", "Override genesis_time with this flag") + cmd.Flags().String(flagChainId, "", "Override chain_id with this flag") + + return cmd +} diff --git a/x/genutil/client/cli/migrate_test.go b/x/genutil/client/cli/migrate_test.go new file mode 100644 index 000000000000..4df0ebdbbc2d --- /dev/null +++ b/x/genutil/client/cli/migrate_test.go @@ -0,0 +1,58 @@ +package cli + +import ( + "io/ioutil" + "path" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" + "github.com/tendermint/tendermint/libs/cli" + "github.com/tendermint/tendermint/libs/log" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/tests" +) + +func setupCmd(genesisTime string, chainId string) *cobra.Command { + c := &cobra.Command{ + Use: "c", + Args: cobra.ArbitraryArgs, + Run: func(_ *cobra.Command, args []string) {}, + } + + c.Flags().String(flagGenesisTime, genesisTime, "") + c.Flags().String(flagChainId, chainId, "") + + return c +} + +func TestMigrateGenesis(t *testing.T) { + home, cleanup := tests.NewTestCaseDir(t) + viper.Set(cli.HomeFlag, home) + viper.Set(client.FlagName, "moniker") + logger := log.NewNopLogger() + cfg, err := tcmd.ParseConfig() + require.Nil(t, err) + ctx := server.NewContext(cfg, logger) + cdc := makeCodec() + + genesisPath := path.Join(home, "genesis.json") + target := "v0.36" + + defer cleanup() + + // Reject if we dont' have the right parameters or genesis does not exists + require.Error(t, MigrateGenesisCmd(ctx, cdc).RunE(nil, []string{target, genesisPath})) + + // Noop migration with minimal genesis + emptyGenesis := []byte(`{"chain_id":"test","app_state":{}}`) + err = ioutil.WriteFile(genesisPath, emptyGenesis, 0644) + require.Nil(t, err) + cmd := setupCmd("", "test2") + require.NoError(t, MigrateGenesisCmd(ctx, cdc).RunE(cmd, []string{target, genesisPath})) + // Every migration function shuold tests its own module separately +} diff --git a/x/genutil/legacy/v036/migrate.go b/x/genutil/legacy/v036/migrate.go new file mode 100644 index 000000000000..66379ab3800e --- /dev/null +++ b/x/genutil/legacy/v036/migrate.go @@ -0,0 +1,26 @@ +package v036 + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/genutil" + v034gov "github.com/cosmos/cosmos-sdk/x/gov/legacy/v034" + v036gov "github.com/cosmos/cosmos-sdk/x/gov/legacy/v036" +) + +// Migrate migrates exported state from v0.34 to a v0.36 genesis state. +func Migrate(appState genutil.AppMap, cdc *codec.Codec) genutil.AppMap { + v034Codec := codec.New() + codec.RegisterCrypto(v034Codec) + v036Codec := codec.New() + codec.RegisterCrypto(v036Codec) + + if appState[v034gov.ModuleName] != nil { + var govState v034gov.GenesisState + v034gov.RegisterCodec(v034Codec) + v034Codec.MustUnmarshalJSON(appState[v034gov.ModuleName], &govState) + v036gov.RegisterCodec(v036Codec) + delete(appState, v034gov.ModuleName) // Drop old key, in case it changed name + appState[v036gov.ModuleName] = v036Codec.MustMarshalJSON(v036gov.MigrateGovernance(govState)) + } + return appState +} diff --git a/x/genutil/legacy/v036/migrate_test.go b/x/genutil/legacy/v036/migrate_test.go new file mode 100644 index 000000000000..68836d513cfb --- /dev/null +++ b/x/genutil/legacy/v036/migrate_test.go @@ -0,0 +1,109 @@ +package v036 + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/go-amino" + + "github.com/cosmos/cosmos-sdk/x/genutil" +) + +var basic034Gov = []byte(` + { + "starting_proposal_id": "2", + "deposits": [ + { + "proposal_id": "1", + "deposit": { + "depositor": "cosmos1grgelyng2v6v3t8z87wu3sxgt9m5s03xvslewd", + "proposal_id": "1", + "amount": [ + { + "denom": "uatom", + "amount": "512000000" + } + ] + } + } + ], + "votes" : [ + { + "proposal_id": "1", + "vote": { + "voter": "cosmos1lktjhnzkpkz3ehrg8psvmwhafg56kfss5597tg", + "proposal_id": "1", + "option": "Yes" + } + } + ], + "proposals": [ + { + "proposal_content": { + "type": "gov/TextProposal", + "value": { + "title": "test", + "description": "test" + } + }, + "proposal_id": "1", + "proposal_status": "Passed", + "final_tally_result": { + "yes": "1", + "abstain": "0", + "no": "0", + "no_with_veto": "0" + }, + "submit_time": "2019-05-03T21:08:25.443199036Z", + "deposit_end_time": "2019-05-17T21:08:25.443199036Z", + "total_deposit": [ + { + "denom": "uatom", + "amount": "512000000" + } + ], + "voting_start_time": "2019-05-04T16:02:33.24680295Z", + "voting_end_time": "2019-05-18T16:02:33.24680295Z" + } + ], + "deposit_params": { + "min_deposit": [ + { + "denom": "uatom", + "amount": "512000000" + } + ], + "max_deposit_period": "1209600000000000" + }, + "voting_params": { + "voting_period": "1209600000000000" + }, + "tally_params": { + "quorum": "0.400000000000000000", + "threshold": "0.500000000000000000", + "veto": "0.334000000000000000" + } + } +`) + +func TestDummyGenesis(t *testing.T) { + genesisDummy := genutil.AppMap{ + "foo": {}, + "bar": []byte(`{"custom": "module"}`), + } + cdc := amino.NewCodec() + migratedDummy := Migrate(genesisDummy, cdc) + + // We should not touch custom modules in the map + require.Equal(t, genesisDummy["foo"], migratedDummy["foo"]) + require.Equal(t, genesisDummy["bar"], migratedDummy["bar"]) +} + +func TestGovGenesis(t *testing.T) { + genesis := genutil.AppMap{ + "gov": basic034Gov, + } + cdc := amino.NewCodec() + + require.NotPanics(t, func() { Migrate(genesis, cdc) }) +} diff --git a/x/genutil/types.go b/x/genutil/types.go new file mode 100644 index 000000000000..f093e1a9663e --- /dev/null +++ b/x/genutil/types.go @@ -0,0 +1,16 @@ +package genutil + +import ( + "encoding/json" + + "github.com/cosmos/cosmos-sdk/codec" +) + +type ( + // AppMap map modules names with their json raw representation + AppMap map[string]json.RawMessage + // MigrationCallback converts a genesis map from the previous version to the targeted one + MigrationCallback func(AppMap, *codec.Codec) AppMap + // MigrationMap defines a mapping from a version to a MigrationCallback + MigrationMap map[string]MigrationCallback +) diff --git a/x/gov/legacy/v034/types.go b/x/gov/legacy/v034/types.go new file mode 100644 index 000000000000..e103d084e376 --- /dev/null +++ b/x/gov/legacy/v034/types.go @@ -0,0 +1,513 @@ +package v034 + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Keys +const ( + // ModuleName is the name of the module + ModuleName = "gov" + + // StoreKey is the store key string for gov + StoreKey = ModuleName + + // RouterKey is the message route for gov + RouterKey = ModuleName + + // QuerierRoute is the querier route for gov + QuerierRoute = ModuleName + + // DefaultParamspace default name for parameter store + DefaultParamspace = ModuleName +) + +type GenesisState struct { + StartingProposalID uint64 `json:"starting_proposal_id"` + Deposits []DepositWithMetadata `json:"deposits"` + Votes []VoteWithMetadata `json:"votes"` + Proposals []Proposal `json:"proposals"` + DepositParams DepositParams `json:"deposit_params"` + VotingParams VotingParams `json:"voting_params"` + TallyParams TallyParams `json:"tally_params"` +} + +type DepositWithMetadata struct { + ProposalID uint64 `json:"proposal_id"` + Deposit Deposit `json:"deposit"` +} + +type VoteWithMetadata struct { + ProposalID uint64 `json:"proposal_id"` + Vote Vote `json:"vote"` +} + +type Deposit struct { + ProposalID uint64 `json:"proposal_id"` // proposalID of the proposal + Depositor sdk.AccAddress `json:"depositor"` // Address of the depositor + Amount sdk.Coins `json:"amount"` // Deposit amount +} + +type Deposits []Deposit + +type Vote struct { + ProposalID uint64 `json:"proposal_id"` // proposalID of the proposal + Voter sdk.AccAddress `json:"voter"` // address of the voter + Option VoteOption `json:"option"` // option from OptionSet chosen by the voter +} + +type Votes []Vote + +// Param around deposits for governance +type DepositParams struct { + MinDeposit sdk.Coins `json:"min_deposit,omitempty"` // Minimum deposit for a proposal to enter voting period. + MaxDepositPeriod time.Duration `json:"max_deposit_period,omitempty"` // Maximum period for Atom holders to deposit on a proposal. Initial value: 2 months +} + +type TallyParams struct { + Quorum sdk.Dec `json:"quorum,omitempty"` // Minimum percentage of total stake needed to vote for a result to be considered valid + Threshold sdk.Dec `json:"threshold,omitempty"` // Minimum proportion of Yes votes for proposal to pass. Initial value: 0.5 + Veto sdk.Dec `json:"veto,omitempty"` // Minimum value of Veto votes to Total votes ratio for proposal to be vetoed. Initial value: 1/3 +} + +type VotingParams struct { + VotingPeriod time.Duration `json:"voting_period,omitempty"` // Length of the voting period. +} + +type TallyResult struct { + Yes sdk.Int `json:"yes"` + Abstain sdk.Int `json:"abstain"` + No sdk.Int `json:"no"` + NoWithVeto sdk.Int `json:"no_with_veto"` +} + +// ---------------------------------------------------------------------------- +// ProposalStatus +// ---------------------------------------------------------------------------- + +// ProposalStatus is a type alias that represents a proposal status as a byte +type ProposalStatus byte + +//nolint +const ( + StatusNil ProposalStatus = 0x00 + StatusDepositPeriod ProposalStatus = 0x01 + StatusVotingPeriod ProposalStatus = 0x02 + StatusPassed ProposalStatus = 0x03 + StatusRejected ProposalStatus = 0x04 + StatusFailed ProposalStatus = 0x05 +) + +// ProposalStatusToString turns a string into a ProposalStatus +func ProposalStatusFromString(str string) (ProposalStatus, error) { + switch str { + case "DepositPeriod": + return StatusDepositPeriod, nil + + case "VotingPeriod": + return StatusVotingPeriod, nil + + case "Passed": + return StatusPassed, nil + + case "Rejected": + return StatusRejected, nil + + case "Failed": + return StatusFailed, nil + + case "": + return StatusNil, nil + + default: + return ProposalStatus(0xff), fmt.Errorf("'%s' is not a valid proposal status", str) + } +} + +// Marshal needed for protobuf compatibility +func (status ProposalStatus) Marshal() ([]byte, error) { + return []byte{byte(status)}, nil +} + +// Unmarshal needed for protobuf compatibility +func (status *ProposalStatus) Unmarshal(data []byte) error { + *status = ProposalStatus(data[0]) + return nil +} + +// Marshals to JSON using string +func (status ProposalStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(status.String()) +} + +// Unmarshals from JSON assuming Bech32 encoding +func (status *ProposalStatus) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + + bz2, err := ProposalStatusFromString(s) + if err != nil { + return err + } + + *status = bz2 + return nil +} + +// String implements the Stringer interface. +func (status ProposalStatus) String() string { + switch status { + case StatusDepositPeriod: + return "DepositPeriod" + + case StatusVotingPeriod: + return "VotingPeriod" + + case StatusPassed: + return "Passed" + + case StatusRejected: + return "Rejected" + + case StatusFailed: + return "Failed" + + default: + return "" + } +} + +// Format implements the fmt.Formatter interface. +// nolint: errcheck +func (status ProposalStatus) Format(s fmt.State, verb rune) { + switch verb { + case 's': + s.Write([]byte(status.String())) + default: + // TODO: Do this conversion more directly + s.Write([]byte(fmt.Sprintf("%v", byte(status)))) + } +} + +// ---------------------------------------------------------------------------- +// VoteOption +// ---------------------------------------------------------------------------- + +type VoteOption byte + +// Vote options +const ( + OptionEmpty VoteOption = 0x00 + OptionYes VoteOption = 0x01 + OptionAbstain VoteOption = 0x02 + OptionNo VoteOption = 0x03 + OptionNoWithVeto VoteOption = 0x04 +) + +// VoteOptionFromString returns a VoteOption from a string. It returns an error +// if the string is invalid. +func VoteOptionFromString(str string) (VoteOption, error) { + switch str { + case "Yes": + return OptionYes, nil + + case "Abstain": + return OptionAbstain, nil + + case "No": + return OptionNo, nil + + case "NoWithVeto": + return OptionNoWithVeto, nil + + default: + return VoteOption(0xff), fmt.Errorf("'%s' is not a valid vote option", str) + } +} + +// Marshal needed for protobuf compatibility. +func (vo VoteOption) Marshal() ([]byte, error) { + return []byte{byte(vo)}, nil +} + +// Unmarshal needed for protobuf compatibility. +func (vo *VoteOption) Unmarshal(data []byte) error { + *vo = VoteOption(data[0]) + return nil +} + +// Marshals to JSON using string. +func (vo VoteOption) MarshalJSON() ([]byte, error) { + return json.Marshal(vo.String()) +} + +// UnmarshalJSON decodes from JSON assuming Bech32 encoding. +func (vo *VoteOption) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + + bz2, err := VoteOptionFromString(s) + if err != nil { + return err + } + + *vo = bz2 + return nil +} + +// String implements the Stringer interface. +func (vo VoteOption) String() string { + switch vo { + case OptionYes: + return "Yes" + case OptionAbstain: + return "Abstain" + case OptionNo: + return "No" + case OptionNoWithVeto: + return "NoWithVeto" + default: + return "" + } +} + +// Format implements the fmt.Formatter interface. +// nolint: errcheck +func (vo VoteOption) Format(s fmt.State, verb rune) { + switch verb { + case 's': + s.Write([]byte(vo.String())) + default: + s.Write([]byte(fmt.Sprintf("%v", byte(vo)))) + } +} + +// ---------------------------------------------------------------------------- +// Tally +// ---------------------------------------------------------------------------- + +// Equals returns if two proposals are equal. +func (tr TallyResult) Equals(comp TallyResult) bool { + return tr.Yes.Equal(comp.Yes) && + tr.Abstain.Equal(comp.Abstain) && + tr.No.Equal(comp.No) && + tr.NoWithVeto.Equal(comp.NoWithVeto) +} + +func (tr TallyResult) String() string { + return fmt.Sprintf(`Tally Result: + Yes: %s + Abstain: %s + No: %s + NoWithVeto: %s`, tr.Yes, tr.Abstain, tr.No, tr.NoWithVeto) +} + +// ---------------------------------------------------------------------------- +// Proposal +// ---------------------------------------------------------------------------- + +type Proposal struct { + ProposalContent `json:"proposal_content"` // Proposal content interface + + ProposalID uint64 `json:"proposal_id"` // ID of the proposal + + Status ProposalStatus `json:"proposal_status"` // Status of the Proposal {Pending, Active, Passed, Rejected} + FinalTallyResult TallyResult `json:"final_tally_result"` // Result of Tallys + + SubmitTime time.Time `json:"submit_time"` // Time of the block where TxGovSubmitProposal was included + DepositEndTime time.Time `json:"deposit_end_time"` // Time that the Proposal would expire if deposit amount isn't met + TotalDeposit sdk.Coins `json:"total_deposit"` // Current deposit on this proposal. Initial value is set at InitialDeposit + + VotingStartTime time.Time `json:"voting_start_time"` // Time of the block where MinDeposit was reached. -1 if MinDeposit is not reached + VotingEndTime time.Time `json:"voting_end_time"` // Time that the VotingPeriod for this proposal will end and votes will be tallied +} + +// nolint +func (p Proposal) String() string { + return fmt.Sprintf(`Proposal %d: + Title: %s + Type: %s + Status: %s + Submit Time: %s + Deposit End Time: %s + Total Deposit: %s + Voting Start Time: %s + Voting End Time: %s + Description: %s`, + p.ProposalID, p.GetTitle(), p.ProposalType(), + p.Status, p.SubmitTime, p.DepositEndTime, + p.TotalDeposit, p.VotingStartTime, p.VotingEndTime, p.GetDescription(), + ) +} + +// ProposalContent is an interface that has title, description, and proposaltype +// that the governance module can use to identify them and generate human readable messages +// ProposalContent can have additional fields, which will handled by ProposalHandlers +// via type assertion, e.g. parameter change amount in ParameterChangeProposal +type ProposalContent interface { + GetTitle() string + GetDescription() string + ProposalType() ProposalKind +} + +// Proposals is an array of proposal +type Proposals []Proposal + +// nolint +func (p Proposals) String() string { + out := "ID - (Status) [Type] Title\n" + for _, prop := range p { + out += fmt.Sprintf("%d - (%s) [%s] %s\n", + prop.ProposalID, prop.Status, + prop.ProposalType(), prop.GetTitle()) + } + return strings.TrimSpace(out) +} + +// Text Proposals +type TextProposal struct { + Title string `json:"title"` // Title of the proposal + Description string `json:"description"` // Description of the proposal +} + +func NewTextProposal(title, description string) TextProposal { + return TextProposal{ + Title: title, + Description: description, + } +} + +// Implements Proposal Interface +var _ ProposalContent = TextProposal{} + +// nolint +func (tp TextProposal) GetTitle() string { return tp.Title } +func (tp TextProposal) GetDescription() string { return tp.Description } +func (tp TextProposal) ProposalType() ProposalKind { return ProposalTypeText } + +// Software Upgrade Proposals +type SoftwareUpgradeProposal struct { + TextProposal +} + +func NewSoftwareUpgradeProposal(title, description string) SoftwareUpgradeProposal { + return SoftwareUpgradeProposal{ + TextProposal: NewTextProposal(title, description), + } +} + +// Implements Proposal Interface +var _ ProposalContent = SoftwareUpgradeProposal{} + +// nolint +func (sup SoftwareUpgradeProposal) ProposalType() ProposalKind { return ProposalTypeSoftwareUpgrade } + +// ProposalQueue +type ProposalQueue []uint64 + +// ProposalKind + +// Type that represents Proposal Type as a byte +type ProposalKind byte + +//nolint +const ( + ProposalTypeNil ProposalKind = 0x00 + ProposalTypeText ProposalKind = 0x01 + ProposalTypeParameterChange ProposalKind = 0x02 + ProposalTypeSoftwareUpgrade ProposalKind = 0x03 +) + +// String to proposalType byte. Returns 0xff if invalid. +func ProposalTypeFromString(str string) (ProposalKind, error) { + switch str { + case "Text": + return ProposalTypeText, nil + case "ParameterChange": + return ProposalTypeParameterChange, nil + case "SoftwareUpgrade": + return ProposalTypeSoftwareUpgrade, nil + default: + return ProposalKind(0xff), fmt.Errorf("'%s' is not a valid proposal type", str) + } +} + +// Marshal needed for protobuf compatibility +func (pt ProposalKind) Marshal() ([]byte, error) { + return []byte{byte(pt)}, nil +} + +// Unmarshal needed for protobuf compatibility +func (pt *ProposalKind) Unmarshal(data []byte) error { + *pt = ProposalKind(data[0]) + return nil +} + +// Marshals to JSON using string +func (pt ProposalKind) MarshalJSON() ([]byte, error) { + return json.Marshal(pt.String()) +} + +// Unmarshals from JSON assuming Bech32 encoding +func (pt *ProposalKind) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + + bz2, err := ProposalTypeFromString(s) + if err != nil { + return err + } + *pt = bz2 + return nil +} + +// Turns VoteOption byte to String +func (pt ProposalKind) String() string { + switch pt { + case ProposalTypeText: + return "Text" + case ProposalTypeParameterChange: + return "ParameterChange" + case ProposalTypeSoftwareUpgrade: + return "SoftwareUpgrade" + default: + return "" + } +} + +// For Printf / Sprintf, returns bech32 when using %s +// nolint: errcheck +func (pt ProposalKind) Format(s fmt.State, verb rune) { + switch verb { + case 's': + s.Write([]byte(pt.String())) + default: + // TODO: Do this conversion more directly + s.Write([]byte(fmt.Sprintf("%v", byte(pt)))) + } +} + +// ---------------------------------------------------------------------------- +// Codec +// ---------------------------------------------------------------------------- + +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterInterface((*ProposalContent)(nil), nil) + cdc.RegisterConcrete(TextProposal{}, "gov/TextProposal", nil) + cdc.RegisterConcrete(SoftwareUpgradeProposal{}, "gov/SoftwareUpgradeProposal", nil) +} diff --git a/x/gov/legacy/v036/migrate.go b/x/gov/legacy/v036/migrate.go new file mode 100644 index 000000000000..9463bad4727f --- /dev/null +++ b/x/gov/legacy/v036/migrate.go @@ -0,0 +1,76 @@ +package v036 + +import ( + v034gov "github.com/cosmos/cosmos-sdk/x/gov/legacy/v034" +) + +func MigrateGovernance(initialState v034gov.GenesisState) GenesisState { + targetGov := GenesisState{ + StartingProposalID: initialState.StartingProposalID, + DepositParams: DepositParams{ + MinDeposit: initialState.DepositParams.MinDeposit, + MaxDepositPeriod: initialState.DepositParams.MaxDepositPeriod, + }, + TallyParams: TallyParams{ + Quorum: initialState.TallyParams.Quorum, + Threshold: initialState.TallyParams.Threshold, + Veto: initialState.TallyParams.Veto, + }, + VotingParams: VotingParams{ + VotingPeriod: initialState.VotingParams.VotingPeriod, + }, + } + + var deposits Deposits + for _, p := range initialState.Deposits { + deposits = append(deposits, Deposit{ + ProposalID: p.Deposit.ProposalID, + Amount: p.Deposit.Amount, + Depositor: p.Deposit.Depositor, + }) + } + + targetGov.Deposits = deposits + + var votes Votes + for _, p := range initialState.Votes { + votes = append(votes, Vote{ + ProposalID: p.Vote.ProposalID, + Option: VoteOption(p.Vote.Option), + Voter: p.Vote.Voter, + }) + } + + targetGov.Votes = votes + + var proposals Proposals + for _, p := range initialState.Proposals { + proposal := Proposal{ + Content: migrateContent(p.ProposalContent), + ProposalID: p.ProposalID, + Status: ProposalStatus(p.Status), + FinalTallyResult: TallyResult(p.FinalTallyResult), + SubmitTime: p.SubmitTime, + DepositEndTime: p.DepositEndTime, + TotalDeposit: p.TotalDeposit, + VotingStartTime: p.VotingStartTime, + VotingEndTime: p.VotingEndTime, + } + + proposals = append(proposals, proposal) + } + + targetGov.Proposals = proposals + return targetGov +} + +func migrateContent(proposalContent v034gov.ProposalContent) (content Content) { + switch proposalContent.ProposalType() { + case v034gov.ProposalTypeText: + return NewTextProposal(proposalContent.GetTitle(), proposalContent.GetDescription()) + case v034gov.ProposalTypeSoftwareUpgrade: + return NewSoftwareUpgradeProposal(proposalContent.GetTitle(), proposalContent.GetDescription()) + default: + return nil + } +} diff --git a/x/gov/legacy/v036/types.go b/x/gov/legacy/v036/types.go new file mode 100644 index 000000000000..a6f6bb0a04c2 --- /dev/null +++ b/x/gov/legacy/v036/types.go @@ -0,0 +1,454 @@ +package v036 + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Keys +const ( + // ModuleName is the name of the module + ModuleName = "gov" + + // StoreKey is the store key string for gov + StoreKey = ModuleName + + // RouterKey is the message route for gov + RouterKey = ModuleName + + // QuerierRoute is the querier route for gov + QuerierRoute = ModuleName + + // DefaultParamspace default name for parameter store + DefaultParamspace = ModuleName +) + +// GenesisState represents v0.34.x genesis state for the governance module. +type GenesisState struct { + StartingProposalID uint64 `json:"starting_proposal_id"` + Deposits Deposits `json:"deposits"` + Votes Votes `json:"votes"` + Proposals []Proposal `json:"proposals"` + DepositParams DepositParams `json:"deposit_params"` + VotingParams VotingParams `json:"voting_params"` + TallyParams TallyParams `json:"tally_params"` +} + +type Deposit struct { + ProposalID uint64 `json:"proposal_id"` // proposalID of the proposal + Depositor sdk.AccAddress `json:"depositor"` // Address of the depositor + Amount sdk.Coins `json:"amount"` // Deposit amount +} + +type Deposits []Deposit + +type Vote struct { + ProposalID uint64 `json:"proposal_id"` // proposalID of the proposal + Voter sdk.AccAddress `json:"voter"` // address of the voter + Option VoteOption `json:"option"` // option from OptionSet chosen by the voter +} + +type Votes []Vote + +// Param around deposits for governance +type DepositParams struct { + MinDeposit sdk.Coins `json:"min_deposit,omitempty"` // Minimum deposit for a proposal to enter voting period. + MaxDepositPeriod time.Duration `json:"max_deposit_period,omitempty"` // Maximum period for Atom holders to deposit on a proposal. Initial value: 2 months +} + +type Content interface { + GetTitle() string + GetDescription() string + ProposalRoute() string + ProposalType() string + ValidateBasic() sdk.Error + String() string +} + +type Proposal struct { + Content `json:"content"` // Proposal content interface + + ProposalID uint64 `json:"id"` // ID of the proposal + Status ProposalStatus `json:"proposal_status"` // Status of the Proposal {Pending, Active, Passed, Rejected} + FinalTallyResult TallyResult `json:"final_tally_result"` // Result of Tallys + + SubmitTime time.Time `json:"submit_time"` // Time of the block where TxGovSubmitProposal was included + DepositEndTime time.Time `json:"deposit_end_time"` // Time that the Proposal would expire if deposit amount isn't met + TotalDeposit sdk.Coins `json:"total_deposit"` // Current deposit on this proposal. Initial value is set at InitialDeposit + + VotingStartTime time.Time `json:"voting_start_time"` // Time of the block where MinDeposit was reached. -1 if MinDeposit is not reached + VotingEndTime time.Time `json:"voting_end_time"` // Time that the VotingPeriod for this proposal will end and votes will be tallied +} + +type Proposals []Proposal +type ProposalQueue []uint64 + +type TallyParams struct { + Quorum sdk.Dec `json:"quorum,omitempty"` // Minimum percentage of total stake needed to vote for a result to be considered valid + Threshold sdk.Dec `json:"threshold,omitempty"` // Minimum proportion of Yes votes for proposal to pass. Initial value: 0.5 + Veto sdk.Dec `json:"veto,omitempty"` // Minimum value of Veto votes to Total votes ratio for proposal to be vetoed. Initial value: 1/3 +} + +// ---------------------------------------------------------------------------- +// ProposalStatus +// ---------------------------------------------------------------------------- + +// ProposalStatus is a type alias that represents a proposal status as a byte +type ProposalStatus byte + +//nolint +const ( + StatusNil ProposalStatus = 0x00 + StatusDepositPeriod ProposalStatus = 0x01 + StatusVotingPeriod ProposalStatus = 0x02 + StatusPassed ProposalStatus = 0x03 + StatusRejected ProposalStatus = 0x04 + StatusFailed ProposalStatus = 0x05 +) + +// ProposalStatusToString turns a string into a ProposalStatus +func ProposalStatusFromString(str string) (ProposalStatus, error) { + switch str { + case "DepositPeriod": + return StatusDepositPeriod, nil + + case "VotingPeriod": + return StatusVotingPeriod, nil + + case "Passed": + return StatusPassed, nil + + case "Rejected": + return StatusRejected, nil + + case "Failed": + return StatusFailed, nil + + case "": + return StatusNil, nil + + default: + return ProposalStatus(0xff), fmt.Errorf("'%s' is not a valid proposal status", str) + } +} + +// Marshal needed for protobuf compatibility +func (status ProposalStatus) Marshal() ([]byte, error) { + return []byte{byte(status)}, nil +} + +// Unmarshal needed for protobuf compatibility +func (status *ProposalStatus) Unmarshal(data []byte) error { + *status = ProposalStatus(data[0]) + return nil +} + +// Marshals to JSON using string +func (status ProposalStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(status.String()) +} + +// Unmarshals from JSON assuming Bech32 encoding +func (status *ProposalStatus) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + + bz2, err := ProposalStatusFromString(s) + if err != nil { + return err + } + + *status = bz2 + return nil +} + +// String implements the Stringer interface. +func (status ProposalStatus) String() string { + switch status { + case StatusDepositPeriod: + return "DepositPeriod" + + case StatusVotingPeriod: + return "VotingPeriod" + + case StatusPassed: + return "Passed" + + case StatusRejected: + return "Rejected" + + case StatusFailed: + return "Failed" + + default: + return "" + } +} + +// Format implements the fmt.Formatter interface. +// nolint: errcheck +func (status ProposalStatus) Format(s fmt.State, verb rune) { + switch verb { + case 's': + s.Write([]byte(status.String())) + default: + // TODO: Do this conversion more directly + s.Write([]byte(fmt.Sprintf("%v", byte(status)))) + } +} + +type VotingParams struct { + VotingPeriod time.Duration `json:"voting_period,omitempty"` // Length of the voting period. +} + +type TallyResult struct { + Yes sdk.Int `json:"yes"` + Abstain sdk.Int `json:"abstain"` + No sdk.Int `json:"no"` + NoWithVeto sdk.Int `json:"no_with_veto"` +} + +// ---------------------------------------------------------------------------- +// VoteOption +// ---------------------------------------------------------------------------- + +type VoteOption byte + +// Vote options +const ( + OptionEmpty VoteOption = 0x00 + OptionYes VoteOption = 0x01 + OptionAbstain VoteOption = 0x02 + OptionNo VoteOption = 0x03 + OptionNoWithVeto VoteOption = 0x04 +) + +// VoteOptionFromString returns a VoteOption from a string. It returns an error +// if the string is invalid. +func VoteOptionFromString(str string) (VoteOption, error) { + switch str { + case "Yes": + return OptionYes, nil + + case "Abstain": + return OptionAbstain, nil + + case "No": + return OptionNo, nil + + case "NoWithVeto": + return OptionNoWithVeto, nil + + default: + return VoteOption(0xff), fmt.Errorf("'%s' is not a valid vote option", str) + } +} + +// Marshal needed for protobuf compatibility. +func (vo VoteOption) Marshal() ([]byte, error) { + return []byte{byte(vo)}, nil +} + +// Unmarshal needed for protobuf compatibility. +func (vo *VoteOption) Unmarshal(data []byte) error { + *vo = VoteOption(data[0]) + return nil +} + +// Marshals to JSON using string. +func (vo VoteOption) MarshalJSON() ([]byte, error) { + return json.Marshal(vo.String()) +} + +// UnmarshalJSON decodes from JSON assuming Bech32 encoding. +func (vo *VoteOption) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + + bz2, err := VoteOptionFromString(s) + if err != nil { + return err + } + + *vo = bz2 + return nil +} + +// String implements the Stringer interface. +func (vo VoteOption) String() string { + switch vo { + case OptionYes: + return "Yes" + case OptionAbstain: + return "Abstain" + case OptionNo: + return "No" + case OptionNoWithVeto: + return "NoWithVeto" + default: + return "" + } +} + +// Format implements the fmt.Formatter interface. +// nolint: errcheck +func (vo VoteOption) Format(s fmt.State, verb rune) { + switch verb { + case 's': + s.Write([]byte(vo.String())) + default: + s.Write([]byte(fmt.Sprintf("%v", byte(vo)))) + } +} + +// ---------------------------------------------------------------------------- +// Tally +// ---------------------------------------------------------------------------- + +// Equals returns if two proposals are equal. +func (tr TallyResult) Equals(comp TallyResult) bool { + return tr.Yes.Equal(comp.Yes) && + tr.Abstain.Equal(comp.Abstain) && + tr.No.Equal(comp.No) && + tr.NoWithVeto.Equal(comp.NoWithVeto) +} + +func (tr TallyResult) String() string { + return fmt.Sprintf(`Tally Result: + Yes: %s + Abstain: %s + No: %s + NoWithVeto: %s`, tr.Yes, tr.Abstain, tr.No, tr.NoWithVeto) +} + +// ---------------------------------------------------------------------------- +// Proposal +// ---------------------------------------------------------------------------- + +// Proposal types +const ( + ProposalTypeText string = "Text" + ProposalTypeSoftwareUpgrade string = "SoftwareUpgrade" +) + +// Text Proposal +type TextProposal struct { + Title string `json:"title"` + Description string `json:"description"` +} + +func NewTextProposal(title, description string) Content { + return TextProposal{title, description} +} + +// Implements Proposal Interface +var _ Content = TextProposal{} + +// nolint +func (tp TextProposal) GetTitle() string { return tp.Title } +func (tp TextProposal) GetDescription() string { return tp.Description } +func (tp TextProposal) ProposalRoute() string { return RouterKey } +func (tp TextProposal) ProposalType() string { return ProposalTypeText } +func (tp TextProposal) ValidateBasic() sdk.Error { return ValidateAbstract(DefaultCodespace, tp) } + +const ( + DefaultCodespace sdk.CodespaceType = "gov" + + CodeInvalidContent sdk.CodeType = 6 + CodeInvalidProposalType sdk.CodeType = 7 +) + +func ErrInvalidProposalContent(cs sdk.CodespaceType, msg string) sdk.Error { + return sdk.NewError(cs, CodeInvalidContent, fmt.Sprintf("invalid proposal content: %s", msg)) +} + +func ErrInvalidProposalType(codespace sdk.CodespaceType, proposalType string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidProposalType, fmt.Sprintf("proposal type '%s' is not valid", proposalType)) +} + +// ValidateAbstract validates a proposal's abstract contents returning an error +// if invalid. +func ValidateAbstract(codespace sdk.CodespaceType, c Content) sdk.Error { + title := c.GetTitle() + if len(strings.TrimSpace(title)) == 0 { + return ErrInvalidProposalContent(codespace, "proposal title cannot be blank") + } + if len(title) > MaxTitleLength { + return ErrInvalidProposalContent(codespace, fmt.Sprintf("proposal title is longer than max length of %d", MaxTitleLength)) + } + + description := c.GetDescription() + if len(description) == 0 { + return ErrInvalidProposalContent(codespace, "proposal description cannot be blank") + } + if len(description) > MaxDescriptionLength { + return ErrInvalidProposalContent(codespace, fmt.Sprintf("proposal description is longer than max length of %d", MaxDescriptionLength)) + } + + return nil +} + +// Constants pertaining to a Content object +const ( + MaxDescriptionLength int = 5000 + MaxTitleLength int = 140 +) + +func (tp TextProposal) String() string { + return fmt.Sprintf(`Text Proposal: + Title: %s + Description: %s +`, tp.Title, tp.Description) +} + +// Software Upgrade Proposals +// TODO: We have to add fields for SUP specific arguments e.g. commit hash, +// upgrade date, etc. +type SoftwareUpgradeProposal struct { + Title string `json:"title"` + Description string `json:"description"` +} + +func NewSoftwareUpgradeProposal(title, description string) Content { + return SoftwareUpgradeProposal{title, description} +} + +// Implements Proposal Interface +var _ Content = SoftwareUpgradeProposal{} + +// nolint +func (sup SoftwareUpgradeProposal) GetTitle() string { return sup.Title } +func (sup SoftwareUpgradeProposal) GetDescription() string { return sup.Description } +func (sup SoftwareUpgradeProposal) ProposalRoute() string { return RouterKey } +func (sup SoftwareUpgradeProposal) ProposalType() string { return ProposalTypeSoftwareUpgrade } +func (sup SoftwareUpgradeProposal) ValidateBasic() sdk.Error { + return ValidateAbstract(DefaultCodespace, sup) +} + +func (sup SoftwareUpgradeProposal) String() string { + return fmt.Sprintf(`Software Upgrade Proposal: + Title: %s + Description: %s +`, sup.Title, sup.Description) +} + +// ---------------------------------------------------------------------------- +// Codec +// ---------------------------------------------------------------------------- + +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterInterface((*Content)(nil), nil) + cdc.RegisterConcrete(TextProposal{}, "cosmos-sdk/TextProposal", nil) + cdc.RegisterConcrete(SoftwareUpgradeProposal{}, "cosmos-sdk/SoftwareUpgradeProposal", nil) +}