diff --git a/x/auth/legacy/v0_38/migrate.go b/x/auth/legacy/v0_38/migrate.go index 35c2c7371..32a5b8346 100644 --- a/x/auth/legacy/v0_38/migrate.go +++ b/x/auth/legacy/v0_38/migrate.go @@ -1,13 +1,52 @@ package v038 import ( - "encoding/json" - v036auth "github.com/cosmos/cosmos-sdk/x/auth/legacy/v0_36" + v036genaccounts "github.com/cosmos/cosmos-sdk/x/genaccounts/legacy/v0_36" ) // Migrate accepts exported genesis state from v0.34 and migrates it to v0.38 // genesis state. -func Migrate(oldGenState v036auth.GenesisState, accounts json.RawMessage) GenesisState { - return NewGenesisState(oldGenState.Params, accounts) +func Migrate(authGenState v036auth.GenesisState, genAccountsGenState v036genaccounts.GenesisState) GenesisState { + accounts := make(GenesisAccounts, len(genAccountsGenState)) + + for i, acc := range genAccountsGenState { + var genAccount GenesisAccount + + baseAccount := NewBaseAccount(acc.Address, acc.Coins.Sort(), acc.AccountNumber, acc.Sequence) + + switch { + case !acc.OriginalVesting.IsZero(): + baseVestingAccount := NewBaseVestingAccount( + baseAccount, acc.OriginalVesting.Sort(), acc.DelegatedFree.Sort(), + acc.DelegatedVesting.Sort(), acc.EndTime, + ) + + if acc.StartTime != 0 && acc.EndTime != 0 { + // continuous vesting account type + genAccount = NewContinuousVestingAccountRaw(baseVestingAccount, acc.StartTime) + } else if acc.EndTime != 0 { + // delayed vesting account type + genAccount = NewDelayedVestingAccountRaw(baseVestingAccount) + } + + case acc.ModuleName != "": + // module account type + genAccount = NewModuleAccount(baseAccount, acc.ModuleName, acc.ModulePermissions...) + + default: + // standard account type + genAccount = baseAccount + } + + accounts[i] = genAccount + } + + accounts = sanitizeGenesisAccounts(accounts) + + if err := validateGenAccounts(accounts); err != nil { + panic(err) + } + + return NewGenesisState(authGenState.Params, accounts) } diff --git a/x/auth/legacy/v0_38/migrate_test.go b/x/auth/legacy/v0_38/migrate_test.go index 67d401c64..e66ac976e 100644 --- a/x/auth/legacy/v0_38/migrate_test.go +++ b/x/auth/legacy/v0_38/migrate_test.go @@ -1,15 +1,23 @@ package v038 import ( - "encoding/json" "testing" + "time" + sdk "github.com/cosmos/cosmos-sdk/types" v034auth "github.com/cosmos/cosmos-sdk/x/auth/legacy/v0_34" v036auth "github.com/cosmos/cosmos-sdk/x/auth/legacy/v0_36" + v036genaccounts "github.com/cosmos/cosmos-sdk/x/genaccounts/legacy/v0_36" "github.com/stretchr/testify/require" ) +func accAddressFromBech32(t *testing.T, addrStr string) sdk.AccAddress { + addr, err := sdk.AccAddressFromBech32(addrStr) + require.NoError(t, err) + return addr +} + func TestMigrate(t *testing.T) { var genesisState GenesisState @@ -20,105 +28,155 @@ func TestMigrate(t *testing.T) { SigVerifyCostED25519: 10, SigVerifyCostSecp256k1: 10, } - rawAccounts := `[ - { - "address": "cosmos1dfp05pasnts7a4lupn889vptjtrxzkk5f7027f", - "coins": [ - { - "denom": "node0token", - "amount": "1000000000" - }, - { - "denom": "stake", - "amount": "500000000" - } - ], - "sequence_number": "0", - "account_number": "0", - "original_vesting": [], - "delegated_free": [], - "delegated_vesting": [], - "start_time": "0", - "end_time": "0", - "module_name": "", - "module_permissions": null - }, - { - "address": "cosmos1f6dangl9ggdhuvkcwhswserr8fzra6vfzfjvh2", - "coins": [ - { - "denom": "node1token", - "amount": "1000000000" - }, - { - "denom": "stake", - "amount": "500000000" - } - ], - "sequence_number": "0", - "account_number": "0", - "original_vesting": [], - "delegated_free": [], - "delegated_vesting": [], - "start_time": "0", - "end_time": "0", - "module_name": "", - "module_permissions": null - }, - { - "address": "cosmos1gudmxhn5anh5m6m2rr4rsfhgvps8fchtgmk7a6", - "coins": [ - { - "denom": "node2token", - "amount": "1000000000" - }, - { - "denom": "stake", - "amount": "500000000" - } - ], - "sequence_number": "0", - "account_number": "0", - "original_vesting": [], - "delegated_free": [], - "delegated_vesting": [], - "start_time": "0", - "end_time": "0", - "module_name": "", - "module_permissions": null - }, - { - "address": "cosmos1kluvs8ff2s3hxad4jpmhvca4crqpcwn9xyhchv", - "coins": [ - { - "denom": "node3token", - "amount": "1000000000" - }, - { - "denom": "stake", - "amount": "500000000" - } - ], - "sequence_number": "0", - "account_number": "0", - "original_vesting": [], - "delegated_free": [], - "delegated_vesting": [], - "start_time": "0", - "end_time": "0", - "module_name": "", - "module_permissions": null - } - ]` + + acc1 := v036genaccounts.GenesisAccount{ + Address: accAddressFromBech32(t, "cosmos1f9xjhxm0plzrh9cskf4qee4pc2xwp0n0556gh0"), + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 400000)), + Sequence: 1, + AccountNumber: 1, + } + acc2 := v036genaccounts.GenesisAccount{ + Address: accAddressFromBech32(t, "cosmos1fl48vsnmsdzcv85q5d2q4z5ajdha8yu34mf0eh"), + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 400000000)), + Sequence: 4, + AccountNumber: 2, + ModuleName: "bonded_tokens_pool", + ModulePermissions: []string{"burner", "staking"}, + } + acc3 := v036genaccounts.GenesisAccount{ + Address: accAddressFromBech32(t, "cosmos17n9sztlhx32tfy0tg0zc2ttmkeeth50yyuv9he"), + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 10000205)), + OriginalVesting: sdk.NewCoins(sdk.NewInt64Coin("stake", 10000205)), + StartTime: time.Now().Unix(), + EndTime: time.Now().Add(48 * time.Hour).Unix(), + Sequence: 5, + AccountNumber: 3, + } + acc4 := v036genaccounts.GenesisAccount{ + Address: accAddressFromBech32(t, "cosmos1fmk5elg4r62mlexd36tqjcwyafs7mek0js5m4d"), + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 10000205)), + OriginalVesting: sdk.NewCoins(sdk.NewInt64Coin("stake", 10000205)), + EndTime: time.Now().Add(48 * time.Hour).Unix(), + Sequence: 15, + AccountNumber: 4, + } require.NotPanics(t, func() { genesisState = Migrate( v036auth.GenesisState{ Params: params, }, - json.RawMessage(rawAccounts), + v036genaccounts.GenesisState{acc1, acc2, acc3, acc4}, ) }) - require.Equal(t, genesisState, GenesisState{Params: params, Accounts: json.RawMessage(rawAccounts)}) + expectedAcc1 := NewBaseAccount(acc1.Address, acc1.Coins, acc1.AccountNumber, acc1.Sequence) + expectedAcc2 := NewModuleAccount( + NewBaseAccount(acc2.Address, acc2.Coins, acc2.AccountNumber, acc2.Sequence), + acc2.ModuleName, acc2.ModulePermissions..., + ) + expectedAcc3 := NewContinuousVestingAccountRaw( + NewBaseVestingAccount( + NewBaseAccount(acc3.Address, acc3.Coins, acc3.AccountNumber, acc3.Sequence), + acc3.OriginalVesting, acc3.DelegatedFree, acc3.DelegatedVesting, acc3.EndTime, + ), + acc3.StartTime, + ) + expectedAcc4 := NewDelayedVestingAccountRaw( + NewBaseVestingAccount( + NewBaseAccount(acc4.Address, acc4.Coins, acc4.AccountNumber, acc4.Sequence), + acc4.OriginalVesting, acc4.DelegatedFree, acc4.DelegatedVesting, acc4.EndTime, + ), + ) + + require.Equal( + t, genesisState, GenesisState{ + Params: params, + Accounts: GenesisAccounts{expectedAcc1, expectedAcc2, expectedAcc3, expectedAcc4}, + }, + ) +} + +func TestMigrateInvalid(t *testing.T) { + testCases := []struct { + name string + acc v036genaccounts.GenesisAccount + }{ + { + "vesting account with no base coins", + v036genaccounts.GenesisAccount{ + Address: accAddressFromBech32(t, "cosmos17n9sztlhx32tfy0tg0zc2ttmkeeth50yyuv9he"), + Coins: sdk.Coins{}, + OriginalVesting: sdk.NewCoins(sdk.NewInt64Coin("stake", 10000205)), + StartTime: time.Now().Unix(), + EndTime: time.Now().Add(48 * time.Hour).Unix(), + Sequence: 5, + AccountNumber: 3, + }, + }, + { + "vesting account with base coins <= original vesting", + v036genaccounts.GenesisAccount{ + Address: accAddressFromBech32(t, "cosmos17n9sztlhx32tfy0tg0zc2ttmkeeth50yyuv9he"), + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 10000205)), + OriginalVesting: sdk.NewCoins(sdk.NewInt64Coin("stake", 50000205)), + StartTime: time.Now().Unix(), + EndTime: time.Now().Add(48 * time.Hour).Unix(), + Sequence: 5, + AccountNumber: 3, + }, + }, + { + "module account with invalid name", + v036genaccounts.GenesisAccount{ + Address: accAddressFromBech32(t, "cosmos1fl48vsnmsdzcv85q5d2q4z5ajdha8yu34mf0eh"), + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 400000000)), + Sequence: 4, + AccountNumber: 2, + ModuleName: " ", + ModulePermissions: []string{"burner", "staking"}, + }, + }, + { + "module account with invalid permissions", + v036genaccounts.GenesisAccount{ + Address: accAddressFromBech32(t, "cosmos1fl48vsnmsdzcv85q5d2q4z5ajdha8yu34mf0eh"), + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 400000000)), + Sequence: 4, + AccountNumber: 2, + ModuleName: "bonded_tokens_pool", + ModulePermissions: []string{""}, + }, + }, + { + "module account with invalid address", + v036genaccounts.GenesisAccount{ + Address: accAddressFromBech32(t, "cosmos17n9sztlhx32tfy0tg0zc2ttmkeeth50yyuv9he"), + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 400000000)), + Sequence: 4, + AccountNumber: 2, + ModuleName: "bonded_tokens_pool", + ModulePermissions: []string{"burner", "staking"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Panics(t, func() { + Migrate( + v036auth.GenesisState{ + Params: v034auth.Params{ + MaxMemoCharacters: 10, + TxSigLimit: 10, + TxSizeCostPerByte: 10, + SigVerifyCostED25519: 10, + SigVerifyCostSecp256k1: 10, + }, + }, + v036genaccounts.GenesisState{tc.acc}, + ) + }) + }) + } } diff --git a/x/auth/legacy/v0_38/types.go b/x/auth/legacy/v0_38/types.go index 7c8f12ef0..90d42133b 100644 --- a/x/auth/legacy/v0_38/types.go +++ b/x/auth/legacy/v0_38/types.go @@ -1,26 +1,262 @@ package v038 -import ( - "encoding/json" +// DONTCOVER +// nolint +import ( + "bytes" + "errors" + "fmt" + "sort" + "strings" + + "github.com/tendermint/tendermint/crypto" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" v034auth "github.com/cosmos/cosmos-sdk/x/auth/legacy/v0_34" ) -// DONTCOVER - -// nolint const ( ModuleName = "auth" ) -type GenesisState struct { - Params v034auth.Params `json:"params"` - Accounts json.RawMessage `json:"accounts"` -} +type ( + // partial interface needed only for amino encoding and sanitization + Account interface { + GetAddress() sdk.AccAddress + GetAccountNumber() uint64 + GetCoins() sdk.Coins + SetCoins(sdk.Coins) error + } -func NewGenesisState(params v034auth.Params, accounts json.RawMessage) GenesisState { + GenesisAccount interface { + Account + + Validate() error + } + + GenesisAccounts []GenesisAccount + + GenesisState struct { + Params v034auth.Params `json:"params" yaml:"params"` + Accounts GenesisAccounts `json:"accounts" yaml:"accounts"` + } + + BaseAccount struct { + Address sdk.AccAddress `json:"address" yaml:"address"` + Coins sdk.Coins `json:"coins" yaml:"coins"` + PubKey crypto.PubKey `json:"public_key" yaml:"public_key"` + AccountNumber uint64 `json:"account_number" yaml:"account_number"` + Sequence uint64 `json:"sequence" yaml:"sequence"` + } + + BaseVestingAccount struct { + *BaseAccount + + OriginalVesting sdk.Coins `json:"original_vesting"` + DelegatedFree sdk.Coins `json:"delegated_free"` + DelegatedVesting sdk.Coins `json:"delegated_vesting"` + + EndTime int64 `json:"end_time"` + } + + ContinuousVestingAccount struct { + *BaseVestingAccount + + StartTime int64 `json:"start_time"` + } + + DelayedVestingAccount struct { + *BaseVestingAccount + } + + ModuleAccount struct { + *BaseAccount + + Name string `json:"name" yaml:"name"` + Permissions []string `json:"permissions" yaml:"permissions"` + } +) + +func NewGenesisState(params v034auth.Params, accounts GenesisAccounts) GenesisState { return GenesisState{ Params: params, Accounts: accounts, } } + +func NewBaseAccountWithAddress(addr sdk.AccAddress) BaseAccount { + return BaseAccount{ + Address: addr, + } +} + +func NewBaseAccount( + address sdk.AccAddress, coins sdk.Coins, accountNumber, sequence uint64, +) *BaseAccount { + + return &BaseAccount{ + Address: address, + Coins: coins, + PubKey: nil, + AccountNumber: accountNumber, + Sequence: sequence, + } +} + +func (acc BaseAccount) GetAddress() sdk.AccAddress { + return acc.Address +} + +func (acc *BaseAccount) GetAccountNumber() uint64 { + return acc.AccountNumber +} + +func (acc *BaseAccount) GetCoins() sdk.Coins { + return acc.Coins +} + +func (acc *BaseAccount) SetCoins(coins sdk.Coins) error { + acc.Coins = coins + return nil +} + +func (acc BaseAccount) Validate() error { + if acc.PubKey != nil && acc.Address != nil && + !bytes.Equal(acc.PubKey.Address().Bytes(), acc.Address.Bytes()) { + return errors.New("pubkey and address pair is invalid") + } + + return nil +} + +func NewBaseVestingAccount( + baseAccount *BaseAccount, originalVesting, delegatedFree, delegatedVesting sdk.Coins, endTime int64, +) *BaseVestingAccount { + + return &BaseVestingAccount{ + BaseAccount: baseAccount, + OriginalVesting: originalVesting, + DelegatedFree: delegatedFree, + DelegatedVesting: delegatedVesting, + EndTime: endTime, + } +} + +func (bva BaseVestingAccount) Validate() error { + if (bva.Coins.IsZero() && !bva.OriginalVesting.IsZero()) || + bva.OriginalVesting.IsAnyGT(bva.Coins) { + return errors.New("vesting amount cannot be greater than total amount") + } + + return bva.BaseAccount.Validate() +} + +func NewContinuousVestingAccountRaw(bva *BaseVestingAccount, startTime int64) *ContinuousVestingAccount { + return &ContinuousVestingAccount{ + BaseVestingAccount: bva, + StartTime: startTime, + } +} + +func (cva ContinuousVestingAccount) Validate() error { + if cva.StartTime >= cva.EndTime { + return errors.New("vesting start-time cannot be before end-time") + } + + return cva.BaseVestingAccount.Validate() +} + +func NewDelayedVestingAccountRaw(bva *BaseVestingAccount) *DelayedVestingAccount { + return &DelayedVestingAccount{ + BaseVestingAccount: bva, + } +} + +func (dva DelayedVestingAccount) Validate() error { + return dva.BaseVestingAccount.Validate() +} + +func NewModuleAddress(name string) sdk.AccAddress { + return sdk.AccAddress(crypto.AddressHash([]byte(name))) +} + +func NewModuleAccount(baseAccount *BaseAccount, name string, permissions ...string) *ModuleAccount { + return &ModuleAccount{ + BaseAccount: baseAccount, + Name: name, + Permissions: permissions, + } +} + +func validatePermissions(permissions ...string) error { + for _, perm := range permissions { + if strings.TrimSpace(perm) == "" { + return fmt.Errorf("module permission is empty") + } + } + + return nil +} + +func (ma ModuleAccount) Validate() error { + if err := validatePermissions(ma.Permissions...); err != nil { + return err + } + + if strings.TrimSpace(ma.Name) == "" { + return errors.New("module account name cannot be blank") + } + + if !ma.Address.Equals(sdk.AccAddress(crypto.AddressHash([]byte(ma.Name)))) { + return fmt.Errorf("address %s cannot be derived from the module name '%s'", ma.Address, ma.Name) + } + + return ma.BaseAccount.Validate() +} + +func sanitizeGenesisAccounts(genAccounts GenesisAccounts) GenesisAccounts { + sort.Slice(genAccounts, func(i, j int) bool { + return genAccounts[i].GetAccountNumber() < genAccounts[j].GetAccountNumber() + }) + + for _, acc := range genAccounts { + if err := acc.SetCoins(acc.GetCoins().Sort()); err != nil { + panic(err) + } + } + + return genAccounts +} + +func validateGenAccounts(genAccounts GenesisAccounts) error { + addrMap := make(map[string]bool, len(genAccounts)) + for _, acc := range genAccounts { + + // check for duplicated accounts + addrStr := acc.GetAddress().String() + if _, ok := addrMap[addrStr]; ok { + return fmt.Errorf("duplicate account found in genesis state; address: %s", addrStr) + } + + addrMap[addrStr] = true + + // check account specific validation + if err := acc.Validate(); err != nil { + return fmt.Errorf("invalid account found in genesis state; address: %s, error: %s", addrStr, err.Error()) + } + } + + return nil +} + +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterInterface((*GenesisAccount)(nil), nil) + cdc.RegisterInterface((*Account)(nil), nil) + cdc.RegisterConcrete(&BaseAccount{}, "cosmos-sdk/Account", nil) + cdc.RegisterConcrete(&BaseVestingAccount{}, "cosmos-sdk/BaseVestingAccount", nil) + cdc.RegisterConcrete(&ContinuousVestingAccount{}, "cosmos-sdk/ContinuousVestingAccount", nil) + cdc.RegisterConcrete(&DelayedVestingAccount{}, "cosmos-sdk/DelayedVestingAccount", nil) + cdc.RegisterConcrete(&ModuleAccount{}, "cosmos-sdk/ModuleAccount", nil) +} diff --git a/x/auth/types/account.go b/x/auth/types/account.go index d004044df..1d4318dab 100644 --- a/x/auth/types/account.go +++ b/x/auth/types/account.go @@ -196,9 +196,10 @@ type BaseVestingAccount struct { EndTime int64 `json:"end_time"` // when the coins become unlocked } -// NewBaseVestingAccount creates a new BaseVestingAccount object -func NewBaseVestingAccount(baseAccount *BaseAccount, originalVesting sdk.Coins, - delegatedFree sdk.Coins, delegatedVesting sdk.Coins, endTime int64) *BaseVestingAccount { +// NewBaseVestingAccount creates a new BaseVestingAccount object. +func NewBaseVestingAccount( + baseAccount *BaseAccount, originalVesting, delegatedFree, delegatedVesting sdk.Coins, endTime int64, +) *BaseVestingAccount { return &BaseVestingAccount{ BaseAccount: baseAccount, @@ -382,9 +383,7 @@ type ContinuousVestingAccount struct { } // NewContinuousVestingAccountRaw creates a new ContinuousVestingAccount object from BaseVestingAccount -func NewContinuousVestingAccountRaw(bva *BaseVestingAccount, - startTime int64) *ContinuousVestingAccount { - +func NewContinuousVestingAccountRaw(bva *BaseVestingAccount, startTime int64) *ContinuousVestingAccount { return &ContinuousVestingAccount{ BaseVestingAccount: bva, StartTime: startTime, diff --git a/x/genutil/legacy/v0_38/migrate.go b/x/genutil/legacy/v0_38/migrate.go index fef2091b4..6d936194b 100644 --- a/x/genutil/legacy/v0_38/migrate.go +++ b/x/genutil/legacy/v0_38/migrate.go @@ -15,13 +15,20 @@ func Migrate(appState genutil.AppMap) genutil.AppMap { v038Codec := codec.New() codec.RegisterCrypto(v038Codec) + v038auth.RegisterCodec(v038Codec) if appState[v036genaccounts.ModuleName] != nil { + // unmarshal relative source genesis application state var authGenState v036auth.GenesisState v036Codec.MustUnmarshalJSON(appState[v036auth.ModuleName], &authGenState) + var genAccountsGenState v036genaccounts.GenesisState + v036Codec.MustUnmarshalJSON(appState[v036genaccounts.ModuleName], &genAccountsGenState) + + // Migrate relative source genesis application state and marshal it into + // the respective key. appState[v038auth.ModuleName] = v038Codec.MustMarshalJSON( - v038auth.Migrate(authGenState, appState[v036genaccounts.ModuleName]), + v038auth.Migrate(authGenState, genAccountsGenState), ) // delete deprecated genaccounts genesis state