From 795d330183aa2782465fb1031b1b23590d2f5af0 Mon Sep 17 00:00:00 2001 From: Amaury <1293565+amaurym@users.noreply.github.com> Date: Tue, 13 Dec 2022 10:23:38 +0100 Subject: [PATCH] fix(gov): Fix v3 votes migrations (#14214) Co-authored-by: Julien Robert Co-authored-by: atheeshp <59333759+atheeshp@users.noreply.github.com> Co-authored-by: Aleksandr Bezobchuk (cherry picked from commit 1bd9cbe9e096a0e456ae83aa9a5e092c1589744c) # Conflicts: # CHANGELOG.md # x/gov/migrations/v3/json_test.go # x/gov/migrations/v3/store.go # x/gov/migrations/v3/store_test.go --- CHANGELOG.md | 5 + x/gov/migrations/v3/json_test.go | 157 ++++++++++++++++++++++++++++++ x/gov/migrations/v3/store.go | 95 ++++++++++++++++++ x/gov/migrations/v3/store_test.go | 95 ++++++++++++++++++ 4 files changed, 352 insertions(+) create mode 100644 x/gov/migrations/v3/json_test.go create mode 100644 x/gov/migrations/v3/store.go create mode 100644 x/gov/migrations/v3/store_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ff59d85f2697..41728a66a3e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,11 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### State Machine Breaking +<<<<<<< HEAD +======= +* (x/gov) [#14214](https://github.com/cosmos/cosmos-sdk/pull/14214) Fix gov v0.46 migration to v1 votes. +* (x/group) [#13742](https://github.com/cosmos/cosmos-sdk/pull/13742) Migrate group policy account from module accounts to base account. +>>>>>>> 1bd9cbe9e (fix(gov): Fix v3 votes migrations (#14214)) * (x/group) [#14071](https://github.com/cosmos/cosmos-sdk/pull/14071) Don't re-tally proposal after voting period end if they have been marked as ACCEPTED or REJECTED. ### API Breaking Changes diff --git a/x/gov/migrations/v3/json_test.go b/x/gov/migrations/v3/json_test.go new file mode 100644 index 000000000000..3801c8d4df85 --- /dev/null +++ b/x/gov/migrations/v3/json_test.go @@ -0,0 +1,157 @@ +package v3_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/cosmos/gogoproto/proto" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/client" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/gov" + v3 "github.com/cosmos/cosmos-sdk/x/gov/migrations/v3" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" +) + +var voter = sdk.MustAccAddressFromBech32("cosmos1fl48vsnmsdzcv85q5d2q4z5ajdha8yu34mf0eh") + +func TestMigrateJSON(t *testing.T) { + encodingConfig := moduletestutil.MakeTestEncodingConfig(gov.AppModuleBasic{}) + clientCtx := client.Context{}. + WithInterfaceRegistry(encodingConfig.InterfaceRegistry). + WithTxConfig(encodingConfig.TxConfig). + WithCodec(encodingConfig.Codec) + + govGenState := v1beta1.DefaultGenesisState() + propTime := time.Unix(1e9, 0) + contentAny, err := codectypes.NewAnyWithValue(v1beta1.NewTextProposal("my title", "my desc").(proto.Message)) + require.NoError(t, err) + govGenState.Proposals = v1beta1.Proposals{ + v1beta1.Proposal{ + ProposalId: 1, + Content: contentAny, + SubmitTime: propTime, + DepositEndTime: propTime, + VotingStartTime: propTime, + VotingEndTime: propTime, + Status: v1beta1.StatusDepositPeriod, + FinalTallyResult: v1beta1.EmptyTallyResult(), + TotalDeposit: sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(123))), + }, + } + govGenState.Votes = v1beta1.Votes{ + v1beta1.Vote{ProposalId: 1, Voter: voter.String(), Option: v1beta1.OptionAbstain}, + v1beta1.Vote{ProposalId: 2, Voter: voter.String(), Options: v1beta1.NewNonSplitVoteOption(v1beta1.OptionNo)}, + } + + migrated, err := v3.MigrateJSON(govGenState) + require.NoError(t, err) + + // Make sure the migrated proposal's Msg signer is the gov acct. + require.Equal(t, + authtypes.NewModuleAddress(types.ModuleName).String(), + migrated.Proposals[0].Messages[0].GetCachedValue().(*v1.MsgExecLegacyContent).Authority, + ) + + bz, err := clientCtx.Codec.MarshalJSON(migrated) + require.NoError(t, err) + + // Indent the JSON bz correctly. + var jsonObj map[string]interface{} + err = json.Unmarshal(bz, &jsonObj) + require.NoError(t, err) + indentedBz, err := json.MarshalIndent(jsonObj, "", "\t") + require.NoError(t, err) + + // Make sure about: + // - Proposals use MsgExecLegacyContent + expected := `{ + "deposit_params": { + "max_deposit_period": "172800s", + "min_deposit": [ + { + "amount": "10000000", + "denom": "stake" + } + ] + }, + "deposits": [], + "params": null, + "proposals": [ + { + "deposit_end_time": "2001-09-09T01:46:40Z", + "final_tally_result": { + "abstain_count": "0", + "no_count": "0", + "no_with_veto_count": "0", + "yes_count": "0" + }, + "id": "1", + "messages": [ + { + "@type": "/cosmos.gov.v1.MsgExecLegacyContent", + "authority": "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", + "content": { + "@type": "/cosmos.gov.v1beta1.TextProposal", + "description": "my desc", + "title": "my title" + } + } + ], + "metadata": "", + "status": "PROPOSAL_STATUS_DEPOSIT_PERIOD", + "submit_time": "2001-09-09T01:46:40Z", + "total_deposit": [ + { + "amount": "123", + "denom": "stake" + } + ], + "voting_end_time": "2001-09-09T01:46:40Z", + "voting_start_time": "2001-09-09T01:46:40Z" + } + ], + "starting_proposal_id": "1", + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto_threshold": "0.334000000000000000" + }, + "votes": [ + { + "metadata": "", + "options": [ + { + "option": "VOTE_OPTION_ABSTAIN", + "weight": "1.000000000000000000" + } + ], + "proposal_id": "1", + "voter": "cosmos1fl48vsnmsdzcv85q5d2q4z5ajdha8yu34mf0eh" + }, + { + "metadata": "", + "options": [ + { + "option": "VOTE_OPTION_NO", + "weight": "1.000000000000000000" + } + ], + "proposal_id": "2", + "voter": "cosmos1fl48vsnmsdzcv85q5d2q4z5ajdha8yu34mf0eh" + } + ], + "voting_params": { + "voting_period": "172800s" + } +}` + + require.Equal(t, expected, string(indentedBz)) +} diff --git a/x/gov/migrations/v3/store.go b/x/gov/migrations/v3/store.go new file mode 100644 index 000000000000..7f3cfe76c66e --- /dev/null +++ b/x/gov/migrations/v3/store.go @@ -0,0 +1,95 @@ +package v3 + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/migrations/v1" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" +) + +// migrateProposals migrates all legacy proposals into MsgExecLegacyContent +// proposals. +func migrateProposals(store sdk.KVStore, cdc codec.BinaryCodec) error { + propStore := prefix.NewStore(store, v1.ProposalsKeyPrefix) + + iter := propStore.Iterator(nil, nil) + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + var oldProp govv1beta1.Proposal + err := cdc.Unmarshal(iter.Value(), &oldProp) + if err != nil { + return err + } + + newProp, err := convertToNewProposal(oldProp) + if err != nil { + return err + } + bz, err := cdc.Marshal(&newProp) + if err != nil { + return err + } + + // Set new value on store. + propStore.Set(iter.Key(), bz) + } + + return nil +} + +// migrateVotes migrates all v1beta1 weighted votes (with sdk.Dec as weight) +// to v1 weighted votes (with string as weight) +func migrateVotes(store sdk.KVStore, cdc codec.BinaryCodec) error { + votesStore := prefix.NewStore(store, v1.VotesKeyPrefix) + + iter := votesStore.Iterator(nil, nil) + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + var oldVote govv1beta1.Vote + err := cdc.Unmarshal(iter.Value(), &oldVote) + if err != nil { + return err + } + + newVote := govv1.Vote{ + ProposalId: oldVote.ProposalId, + Voter: oldVote.Voter, + } + newOptions := make([]*govv1.WeightedVoteOption, len(oldVote.Options)) + for i, o := range oldVote.Options { + newOptions[i] = &govv1.WeightedVoteOption{ + Option: govv1.VoteOption(o.Option), + Weight: o.Weight.String(), // Convert to decimal string + } + } + newVote.Options = newOptions + bz, err := cdc.Marshal(&newVote) + if err != nil { + return err + } + + // Set new value on store. + votesStore.Set(iter.Key(), bz) + } + + return nil +} + +// MigrateStore performs in-place store migrations from v2 (v0.43) to v3 (v0.46). The +// migration includes: +// +// - Migrate proposals to be Msg-based. +func MigrateStore(ctx sdk.Context, storeKey storetypes.StoreKey, cdc codec.BinaryCodec) error { + store := ctx.KVStore(storeKey) + + if err := migrateVotes(store, cdc); err != nil { + return err + } + + return migrateProposals(store, cdc) +} diff --git a/x/gov/migrations/v3/store_test.go b/x/gov/migrations/v3/store_test.go new file mode 100644 index 000000000000..95612a718547 --- /dev/null +++ b/x/gov/migrations/v3/store_test.go @@ -0,0 +1,95 @@ +package v3_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/cosmos/cosmos-sdk/x/gov" + v1gov "github.com/cosmos/cosmos-sdk/x/gov/migrations/v1" + v3gov "github.com/cosmos/cosmos-sdk/x/gov/migrations/v3" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + "github.com/cosmos/cosmos-sdk/x/upgrade" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" +) + +func TestMigrateStore(t *testing.T) { + cdc := moduletestutil.MakeTestEncodingConfig(upgrade.AppModuleBasic{}, gov.AppModuleBasic{}).Codec + govKey := sdk.NewKVStoreKey("gov") + ctx := testutil.DefaultContext(govKey, sdk.NewTransientStoreKey("transient_test")) + store := ctx.KVStore(govKey) + + propTime := time.Unix(1e9, 0) + + // Create 2 proposals + prop1, err := v1beta1.NewProposal(v1beta1.NewTextProposal("my title 1", "my desc 1"), 1, propTime, propTime) + require.NoError(t, err) + prop1Bz, err := cdc.Marshal(&prop1) + require.NoError(t, err) + prop2, err := v1beta1.NewProposal(upgradetypes.NewSoftwareUpgradeProposal("my title 2", "my desc 2", upgradetypes.Plan{ + Name: "my plan 2", + }), 2, propTime, propTime) + require.NoError(t, err) + prop2Bz, err := cdc.Marshal(&prop2) + require.NoError(t, err) + + store.Set(v1gov.ProposalKey(prop1.ProposalId), prop1Bz) + store.Set(v1gov.ProposalKey(prop2.ProposalId), prop2Bz) + + // Vote on prop 1 + options := []v1beta1.WeightedVoteOption{ + {Option: v1beta1.OptionNo, Weight: sdk.MustNewDecFromStr("0.3")}, + {Option: v1beta1.OptionYes, Weight: sdk.MustNewDecFromStr("0.7")}, + } + vote1 := v1beta1.NewVote(1, voter, options) + vote1Bz := cdc.MustMarshal(&vote1) + store.Set(v1gov.VoteKey(1, voter), vote1Bz) + + // Run migrations. + err = v3gov.MigrateStore(ctx, govKey, cdc) + require.NoError(t, err) + + var newProp1 v1.Proposal + err = cdc.Unmarshal(store.Get(v1gov.ProposalKey(prop1.ProposalId)), &newProp1) + require.NoError(t, err) + compareProps(t, prop1, newProp1) + + var newProp2 v1.Proposal + err = cdc.Unmarshal(store.Get(v1gov.ProposalKey(prop2.ProposalId)), &newProp2) + require.NoError(t, err) + compareProps(t, prop2, newProp2) + + var newVote1 v1.Vote + err = cdc.Unmarshal(store.Get(v1gov.VoteKey(prop1.ProposalId, voter)), &newVote1) + require.NoError(t, err) + // Without the votes migration, we would have 300000000000000000 in state, + // because of how sdk.Dec stores itself in state. + require.Equal(t, "0.300000000000000000", newVote1.Options[0].Weight) + require.Equal(t, "0.700000000000000000", newVote1.Options[1].Weight) +} + +func compareProps(t *testing.T, oldProp v1beta1.Proposal, newProp v1.Proposal) { + require.Equal(t, oldProp.ProposalId, newProp.Id) + require.Equal(t, oldProp.TotalDeposit.String(), sdk.Coins(newProp.TotalDeposit).String()) + require.Equal(t, oldProp.Status.String(), newProp.Status.String()) + require.Equal(t, oldProp.FinalTallyResult.Yes.String(), newProp.FinalTallyResult.YesCount) + require.Equal(t, oldProp.FinalTallyResult.No.String(), newProp.FinalTallyResult.NoCount) + require.Equal(t, oldProp.FinalTallyResult.NoWithVeto.String(), newProp.FinalTallyResult.NoWithVetoCount) + require.Equal(t, oldProp.FinalTallyResult.Abstain.String(), newProp.FinalTallyResult.AbstainCount) + + newContent := newProp.Messages[0].GetCachedValue().(*v1.MsgExecLegacyContent).Content.GetCachedValue().(v1beta1.Content) + require.Equal(t, oldProp.Content.GetCachedValue().(v1beta1.Content), newContent) + + // Compare UNIX times, as a simple Equal gives difference between Local and + // UTC times. + // ref: https://github.com/golang/go/issues/19486#issuecomment-292968278 + require.Equal(t, oldProp.SubmitTime.Unix(), newProp.SubmitTime.Unix()) + require.Equal(t, oldProp.DepositEndTime.Unix(), newProp.DepositEndTime.Unix()) + require.Equal(t, oldProp.VotingStartTime.Unix(), newProp.VotingStartTime.Unix()) + require.Equal(t, oldProp.VotingEndTime.Unix(), newProp.VotingEndTime.Unix()) +}