diff --git a/app/consumer/genesis.go b/app/consumer/genesis.go index 11f008fb7a..8c8c9ceff3 100644 --- a/app/consumer/genesis.go +++ b/app/consumer/genesis.go @@ -5,15 +5,16 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "github.com/spf13/cobra" + "golang.org/x/exp/maps" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/version" - consumerTypes "github.com/cosmos/interchain-security/v3/x/ccv/consumer/types" "github.com/cosmos/interchain-security/v3/x/ccv/types" ) @@ -27,9 +28,30 @@ import ( // object provided to it during init. type GenesisState map[string]json.RawMessage -// Migration of consumer genesis content as it is exported from a provider version v1,2,3 +// Map of supported versions for consumer genesis transformation +type IcsVersion string + +const ( + v2_x IcsVersion = "v2.x" + v3_0_x IcsVersion = "v3.0.x" + v3_1_x IcsVersion = "v3.1.x" + v3_2_x IcsVersion = "v3.2.x" + v3_3_x IcsVersion = "v3.3.x" + v4_x_x IcsVersion = "v4.x" +) + +var TransformationVersions map[string]IcsVersion = map[string]IcsVersion{ + "v2.x": v2_x, + "v3.0.x": v3_0_x, + "v3.1.x": v3_1_x, + "v3.2.x": v3_2_x, + "v3.3.x": v3_3_x, + "v4.x": v4_x_x, +} + +// Transformation of consumer genesis content as it is exported from a provider version v1,2,3 // to a format readable by current consumer implementation. -func transform(jsonRaw []byte, ctx client.Context) (json.RawMessage, error) { +func transformToNew(jsonRaw []byte, ctx client.Context) (json.RawMessage, error) { // v1,2,3 uses deprecated fields of GenesisState type oldConsumerGenesis := consumerTypes.GenesisState{} err := ctx.Codec.UnmarshalJSON(jsonRaw, &oldConsumerGenesis) @@ -37,17 +59,20 @@ func transform(jsonRaw []byte, ctx client.Context) (json.RawMessage, error) { return nil, fmt.Errorf("reading consumer genesis data failed: %s", err) } - // some sanity checks for v2 transformation - if len(oldConsumerGenesis.Provider.InitialValSet) > 0 { - return nil, fmt.Errorf("invalid source version. Unexpected element 'provider.initial_val_set'") + initialValSet := oldConsumerGenesis.InitialValSet + // transformation from >= v3.3.x + if len(initialValSet) == 0 { + initialValSet = oldConsumerGenesis.Provider.InitialValSet } - if oldConsumerGenesis.Provider.ClientState != nil { - return nil, fmt.Errorf("invalid source version. Unexpected element 'provider.client_state'") + clientState := oldConsumerGenesis.ProviderClientState + if clientState == nil { + clientState = oldConsumerGenesis.Provider.ClientState } - if oldConsumerGenesis.Provider.ConsensusState != nil { - return nil, fmt.Errorf("invalid source version. Unexpected element 'provider.consensus_state'") + consensusState := oldConsumerGenesis.ProviderConsensusState + if consensusState == nil { + consensusState = oldConsumerGenesis.Provider.ConsensusState } // Use DefaultRetryDelayPeriod if not set @@ -55,14 +80,14 @@ func transform(jsonRaw []byte, ctx client.Context) (json.RawMessage, error) { oldConsumerGenesis.Params.RetryDelayPeriod = types.DefaultRetryDelayPeriod } - // Version 2 of provider genesis data fills up deprecated fields - // ProviderClientState, ConsensusState and InitialValSet + // Versions before v3.3.x of provider genesis data fills up deprecated fields + // ProviderClientState, ConsensusState and InitialValSet in type GenesisState newGenesis := types.ConsumerGenesisState{ Params: oldConsumerGenesis.Params, Provider: types.ProviderInfo{ - ClientState: oldConsumerGenesis.ProviderClientState, - ConsensusState: oldConsumerGenesis.ProviderConsensusState, - InitialValSet: oldConsumerGenesis.InitialValSet, + ClientState: clientState, + ConsensusState: consensusState, + InitialValSet: initialValSet, }, NewChain: oldConsumerGenesis.NewChain, } @@ -74,19 +99,210 @@ func transform(jsonRaw []byte, ctx client.Context) (json.RawMessage, error) { return newJson, nil } +// Transformation of consumer genesis content as it is exported by current provider version +// to a format supported by consumer version v3.3.x +func transformToV33(jsonRaw []byte, ctx client.Context) ([]byte, error) { + // v1,2,3 uses deprecated fields of GenesisState type + srcConGen := consumerTypes.GenesisState{} + err := ctx.Codec.UnmarshalJSON(jsonRaw, &srcConGen) + if err != nil { + return nil, fmt.Errorf("reading consumer genesis data failed: %s", err) + } + + // Remove retry_delay_period from 'params' + params, err := ctx.Codec.MarshalJSON(&srcConGen.Params) + if err != nil { + return nil, err + } + tmp := map[string]json.RawMessage{} + if err := json.Unmarshal(params, &tmp); err != nil { + return nil, fmt.Errorf("unmarshalling 'params' failed: %v", err) + } + _, exists := tmp["retry_delay_period"] + if exists { + delete(tmp, "retry_delay_period") + } + params, err = json.Marshal(tmp) + if err != nil { + return nil, err + } + + // Marshal GenesisState and patch 'params' value + result, err := ctx.Codec.MarshalJSON(&srcConGen) + if err != nil { + return nil, err + } + genState := map[string]json.RawMessage{} + if err := json.Unmarshal(result, &genState); err != nil { + return nil, fmt.Errorf("unmarshalling 'GenesisState' failed: %v", err) + } + genState["params"] = params + + result, err = json.Marshal(genState) + if err != nil { + return nil, fmt.Errorf("marshalling transformation result failed: %v", err) + } + return result, nil +} + +// Transformation of consumer genesis content as it is exported from current provider version +// to a format readable by consumer implementation of version v2.x +// Use removePreHashKey to remove prehash_key_before_comparison from result. +func transformToV2(jsonRaw []byte, ctx client.Context, removePreHashKey bool) (json.RawMessage, error) { + + // populate deprecated fields of GenesisState used by version v2.x + srcConGen := consumerTypes.GenesisState{} + err := ctx.Codec.UnmarshalJSON(jsonRaw, &srcConGen) + if err != nil { + return nil, fmt.Errorf("reading consumer genesis data failed: %s", err) + } + + // remove retry_delay_period from 'params' if present (introduced in v4.x) + params, err := ctx.Codec.MarshalJSON(&srcConGen.Params) + if err != nil { + return nil, err + } + paramsMap := map[string]json.RawMessage{} + if err := json.Unmarshal(params, ¶msMap); err != nil { + return nil, fmt.Errorf("unmarshalling 'params' failed: %v", err) + } + _, exists := paramsMap["retry_delay_period"] + if exists { + delete(paramsMap, "retry_delay_period") + } + params, err = json.Marshal(paramsMap) + if err != nil { + return nil, err + } + + // marshal GenesisState and patch 'params' value + result, err := ctx.Codec.MarshalJSON(&srcConGen) + if err != nil { + return nil, err + } + genState := map[string]json.RawMessage{} + if err := json.Unmarshal(result, &genState); err != nil { + return nil, fmt.Errorf("unmarshalling 'GenesisState' failed: %v", err) + } + genState["params"] = params + + provider, err := ctx.Codec.MarshalJSON(&srcConGen.Provider) + if err != nil { + return nil, fmt.Errorf("unmarshalling 'Provider' failed: %v", err) + } + providerMap := map[string]json.RawMessage{} + if err := json.Unmarshal(provider, &providerMap); err != nil { + return nil, fmt.Errorf("unmarshalling 'provider' failed: %v", err) + } + + // patch .initial_val_set form .provider.initial_val_set if needed + if len(srcConGen.Provider.InitialValSet) > 0 { + valSet, exists := providerMap["initial_val_set"] + if !exists { + return nil, fmt.Errorf("'initial_val_set' not found in provider data") + } + _, exists = genState["initial_val_set"] + if exists { + genState["initial_val_set"] = valSet + } + } + + // patch .provider_consensus_state from provider.consensus_state if needed + if srcConGen.Provider.ConsensusState != nil { + valSet, exists := providerMap["consensus_state"] + if !exists { + return nil, fmt.Errorf("'consensus_state' not found in provider data") + } + _, exists = genState["provider_consensus_state"] + if exists { + genState["provider_consensus_state"] = valSet + } + } + + // patch .provider_client_state from provider.client_state if needed + if srcConGen.Provider.ClientState != nil { + clientState, exists := providerMap["client_state"] + if !exists { + return nil, fmt.Errorf("'client_state' not found in provider data") + } + _, exists = genState["provider_client_state"] + if exists { + genState["provider_client_state"] = clientState + } + } + + // delete .provider entry (introduced in v3.3.x) + delete(genState, "provider") + + // Marshall final result + result, err = json.Marshal(genState) + if err != nil { + return nil, fmt.Errorf("marshalling transformation result failed: %v", err) + } + + if removePreHashKey { + // remove all `prehash_key_before_comparison` entries not supported in v2.x (see ics23) + re := regexp.MustCompile(`,\s*"prehash_key_before_comparison"\s*:\s*(false|true)`) + result = re.ReplaceAll(result, []byte{}) + } + return result, nil +} + +// transformGenesis transforms ccv consumer genesis data to the specified target version +// Returns the transformed data or an error in case the transformation failed or the format is not supported by current implementation +func transformGenesis(ctx client.Context, targetVersion IcsVersion, jsonRaw []byte) (json.RawMessage, error) { + var newConsumerGenesis json.RawMessage = nil + var err error = nil + + switch targetVersion { + // v2.x, v3.0-v3.2 share same consumer genesis type + case v2_x: + newConsumerGenesis, err = transformToV2(jsonRaw, ctx, true) + case v3_0_x, v3_1_x, v3_2_x: + // same as v2 replacement without need of `prehash_key_before_comparison` removal + newConsumerGenesis, err = transformToV2(jsonRaw, ctx, false) + case v3_3_x: + newConsumerGenesis, err = transformToV33(jsonRaw, ctx) + case v4_x_x: + newConsumerGenesis, err = transformToNew(jsonRaw, ctx) + default: + err = fmt.Errorf("unsupported target version '%s'. Run %s --help", + targetVersion, version.AppName) + } + + if err != nil { + return nil, fmt.Errorf("transformation failed: %v", err) + } + return newConsumerGenesis, err +} + // Transform a consumer genesis json file exported from a given ccv provider version -// to a consumer genesis json format supported by current ccv consumer version. +// to a consumer genesis json format supported by current ccv consumer version or v2.x +// This allows user to patch consumer genesis of +// - current implementation from exports of provider of < v3.3.x +// - v2.x from exports of provider >= v3.2.x +// // Result will be written to defined output. func TransformConsumerGenesis(cmd *cobra.Command, args []string) error { - sourceFile := args[0] + sourceFile := args[0] jsonRaw, err := os.ReadFile(filepath.Clean(sourceFile)) if err != nil { return err } clientCtx := client.GetClientContextFromCmd(cmd) - newConsumerGenesis, err := transform(jsonRaw, clientCtx) + version, err := cmd.Flags().GetString("to") + if err != nil { + return fmt.Errorf("error getting targetVersion %v", err) + } + targetVersion, exists := TransformationVersions[version] + if !exists { + return fmt.Errorf("unsupported target version '%s'", version) + } + + // try to transform data to target format + newConsumerGenesis, err := transformGenesis(clientCtx, targetVersion, jsonRaw) if err != nil { return err } @@ -114,17 +330,25 @@ func NewDefaultGenesisState(cdc codec.JSONCodec) GenesisState { // provider version v1,v2 or v3 to a JSON format supported by this consumer version. func GetConsumerGenesisTransformCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "transform [genesis-file]", - Short: "Transform CCV consumer genesis from an older provider version not supporting current format", + Use: "transform [-to version] genesis-file", + Short: "Transform CCV consumer genesis data exported to a specific target format", Long: strings.TrimSpace( - fmt.Sprintf(`Transform the consumer genesis file from a provider version v1,v2 or v3 to a version supported by this consumer. Result is printed to STDOUT. + fmt.Sprintf(` +Transform the consumer genesis data exported from a provider version v1,v2, v3, v4 to a specified consumer target version. +The result is printed to STDOUT. + +Note: Content to be transformed is not the consumer genesis file itself but the exported content from provider chain which is used to patch the consumer genesis file! Example: -$ %s transform /path/to/ccv_consumer_genesis.json`, version.AppName), +$ %s transform /path/to/ccv_consumer_genesis.json +$ %s --to v2.x transform /path/to/ccv_consumer_genesis.json +`, version.AppName, version.AppName), ), - Args: cobra.ExactArgs(1), + Args: cobra.RangeArgs(1, 2), RunE: TransformConsumerGenesis, } - + cmd.Flags().String("to", string(v4_x_x), + fmt.Sprintf("target version for consumer genesis. Supported versions %s", + maps.Keys(TransformationVersions))) return cmd } diff --git a/app/consumer/genesis_test.go b/app/consumer/genesis_test.go index 2ab5be2061..9c5df67834 100644 --- a/app/consumer/genesis_test.go +++ b/app/consumer/genesis_test.go @@ -3,7 +3,7 @@ package app_test import ( "bytes" "context" - "encoding/json" + "fmt" "io/fs" "os" "path/filepath" @@ -24,7 +24,7 @@ import ( // Testdata mapping consumer genesis exports to a provider module version as // used by transformation function for consumer genesis content. var consumerGenesisStates map[string]string = map[string]string{ - "v2": ` + "v2.x": ` { "params": { "enabled": true, @@ -147,6 +147,259 @@ var consumerGenesisStates map[string]string = map[string]string{ } `, + "v3.3.x": ` + { + "params": { + "enabled": true, + "blocks_per_distribution_transmission": "1000", + "distribution_transmission_channel": "", + "provider_fee_pool_addr_str": "", + "ccv_timeout_period": "2419200s", + "transfer_timeout_period": "3600s", + "consumer_redistribution_fraction": "0.75", + "historical_entries": "10000", + "unbonding_period": "1209600s", + "soft_opt_out_threshold": "0.05", + "reward_denoms": [], + "provider_reward_denoms": [] + }, + "provider": { + "client_state": { + "chain_id": "provi", + "trust_level": { + "numerator": "1", + "denominator": "3" + }, + "trusting_period": "1197504s", + "unbonding_period": "1814400s", + "max_clock_drift": "10s", + "frozen_height": { + "revision_number": "0", + "revision_height": "0" + }, + "latest_height": { + "revision_number": "0", + "revision_height": "20" + }, + "proof_specs": [ + { + "leaf_spec": { + "hash": "SHA256", + "prehash_key": "NO_HASH", + "prehash_value": "SHA256", + "length": "VAR_PROTO", + "prefix": "AA==" + }, + "inner_spec": { + "child_order": [ + 0, + 1 + ], + "child_size": 33, + "min_prefix_length": 4, + "max_prefix_length": 12, + "empty_child": null, + "hash": "SHA256" + }, + "max_depth": 0, + "min_depth": 0, + "prehash_key_before_comparison": false + }, + { + "leaf_spec": { + "hash": "SHA256", + "prehash_key": "NO_HASH", + "prehash_value": "SHA256", + "length": "VAR_PROTO", + "prefix": "AA==" + }, + "inner_spec": { + "child_order": [ + 0, + 1 + ], + "child_size": 32, + "min_prefix_length": 1, + "max_prefix_length": 1, + "empty_child": null, + "hash": "SHA256" + }, + "max_depth": 0, + "min_depth": 0, + "prehash_key_before_comparison": false + } + ], + "upgrade_path": [ + "upgrade", + "upgradedIBCState" + ], + "allow_update_after_expiry": false, + "allow_update_after_misbehaviour": false + }, + "consensus_state": { + "timestamp": "2023-12-15T09:25:46.098392003Z", + "root": { + "hash": "0aoNOwWy67aQKs2r+FcDf2RxIq2UJtBb3g9ZWn0Gkas=" + }, + "next_validators_hash": "632730A03DEF630F77B61DF4092629007AE020B789713158FABCB104962FA54F" + }, + "initial_val_set": [ + { + "pub_key": { + "ed25519": "RrclQz9bIhkIy/gfL485g3PYMeiIku4qeo495787X10=" + }, + "power": "500" + }, + { + "pub_key": { + "ed25519": "Ui5Gf1+mtWUdH8u3xlmzdKID+F3PK0sfXZ73GZ6q6is=" + }, + "power": "500" + }, + { + "pub_key": { + "ed25519": "mAN6RXYxSM4MNGSIriYiS7pHuwAcOHDQAy9/wnlSzOI=" + }, + "power": "500" + } + ] + }, + "new_chain": true + } + `, + "v4.x": ` + { + "params": { + "enabled": true, + "blocks_per_distribution_transmission": "1000", + "distribution_transmission_channel": "", + "provider_fee_pool_addr_str": "", + "ccv_timeout_period": "2419200s", + "transfer_timeout_period": "3600s", + "consumer_redistribution_fraction": "0.75", + "historical_entries": "10000", + "unbonding_period": "1209600s", + "soft_opt_out_threshold": "0.05", + "reward_denoms": [], + "provider_reward_denoms": [], + "retry_delay_period": "3600s" + }, + "provider": { + "client_state": { + "chain_id": "provi", + "trust_level": { + "numerator": "1", + "denominator": "3" + }, + "trusting_period": "1197504s", + "unbonding_period": "1814400s", + "max_clock_drift": "10s", + "frozen_height": { + "revision_number": "0", + "revision_height": "0" + }, + "latest_height": { + "revision_number": "0", + "revision_height": "20" + }, + "proof_specs": [ + { + "leaf_spec": { + "hash": "SHA256", + "prehash_key": "NO_HASH", + "prehash_value": "SHA256", + "length": "VAR_PROTO", + "prefix": "AA==" + }, + "inner_spec": { + "child_order": [ + 0, + 1 + ], + "child_size": 33, + "min_prefix_length": 4, + "max_prefix_length": 12, + "empty_child": null, + "hash": "SHA256" + }, + "max_depth": 0, + "min_depth": 0, + "prehash_key_before_comparison": false + }, + { + "leaf_spec": { + "hash": "SHA256", + "prehash_key": "NO_HASH", + "prehash_value": "SHA256", + "length": "VAR_PROTO", + "prefix": "AA==" + }, + "inner_spec": { + "child_order": [ + 0, + 1 + ], + "child_size": 32, + "min_prefix_length": 1, + "max_prefix_length": 1, + "empty_child": null, + "hash": "SHA256" + }, + "max_depth": 0, + "min_depth": 0, + "prehash_key_before_comparison": false + } + ], + "upgrade_path": [ + "upgrade", + "upgradedIBCState" + ], + "allow_update_after_expiry": false, + "allow_update_after_misbehaviour": false + }, + "consensus_state": { + "timestamp": "2023-12-15T09:57:02.687079137Z", + "root": { + "hash": "EH9YbrWC3Qojy8ycl5GhOdVEC1ifPIGUUItL70bTkHo=" + }, + "next_validators_hash": "632730A03DEF630F77B61DF4092629007AE020B789713158FABCB104962FA54F" + }, + "initial_val_set": [ + { + "pub_key": { + "ed25519": "RrclQz9bIhkIy/gfL485g3PYMeiIku4qeo495787X10=" + }, + "power": "500" + }, + { + "pub_key": { + "ed25519": "Ui5Gf1+mtWUdH8u3xlmzdKID+F3PK0sfXZ73GZ6q6is=" + }, + "power": "500" + }, + { + "pub_key": { + "ed25519": "mAN6RXYxSM4MNGSIriYiS7pHuwAcOHDQAy9/wnlSzOI=" + }, + "power": "500" + } + ] + }, + "new_chain": true + } + `, +} + +// creates ccv consumer genesis data content for a given version +// as it was exported from a provider +func createConsumerDataGenesisFile(t *testing.T, version string) string { + filePath := filepath.Join(t.TempDir(), fmt.Sprintf("ConsumerGenesis_%s.json", version)) + err := os.WriteFile( + filePath, + []byte(consumerGenesisStates[version]), + fs.FileMode(0o644)) + require.NoError(t, err, "Error creating source genesis data for version %s", version) + return filePath } func getClientCtx() client.Context { @@ -160,7 +413,8 @@ func getClientCtx() client.Context { WithAccountRetriever(types.AccountRetriever{}) } -// Setup client context +// getGenesisTransformCmd sets up client context and returns +// genesis transformation command (UUT) func getGenesisTransformCmd() (*cobra.Command, error) { cmd := app.GetConsumerGenesisTransformCmd() clientCtx := getClientCtx() @@ -170,52 +424,247 @@ func getGenesisTransformCmd() (*cobra.Command, error) { return cmd, err } +// transformConsumerGenesis transform json content in a given file and return +// the consumer genesis transformation result or an error +// - filePath file with consumer genesis content in json format +// - version ICS version to which the data should be transformed to +func transformConsumerGenesis(filePath string, version *string) ([]byte, error) { + cmd, err := getGenesisTransformCmd() + if err != nil { + return nil, fmt.Errorf("Error setting up transformation command: %s", err) + } + + args := []string{} + if version != nil { + args = append(args, fmt.Sprintf("--to=%s", *version)) + } + args = append(args, filePath) + + cmd.SetArgs(args) + + result := new(bytes.Buffer) + cmd.SetOutput(result) + _, err = cmd.ExecuteC() + if err != nil { + return nil, fmt.Errorf("Error running transformation command: %v", err) + } + return result.Bytes(), nil +} + // Check transformation of a version 2 ConsumerGenesis export to // consumer genesis json format used by current consumer implementation. -func TestConsumerGenesisTransformationV2(t *testing.T) { - version := "v2" - filePath := filepath.Join(t.TempDir(), "oldConsumerGenesis.json") +func TestConsumerGenesisTransformationFromV2ToCurrent(t *testing.T) { + version := "v2.x" + ctx := getClientCtx() - err := os.WriteFile( - filePath, - []byte(consumerGenesisStates[version]), - fs.FileMode(0o644)) + srcGenesis := consumerTypes.GenesisState{} + err := ctx.Codec.UnmarshalJSON([]byte(consumerGenesisStates[version]), &srcGenesis) + require.NoError(t, err, "Error parsing old version of ccv genesis content for consumer") + + filePath := createConsumerDataGenesisFile(t, version) + defer os.Remove(filePath) + resultGenesis := consumerTypes.GenesisState{} + result, err := transformConsumerGenesis(filePath, nil) + require.NoError(t, err) + err = ctx.Codec.UnmarshalJSON(result, &resultGenesis) require.NoError(t, err) + + // Some basic sanity checks on the content. + require.NotNil(t, resultGenesis.Provider.ClientState) + require.Equal(t, "cosmoshub-4", resultGenesis.Provider.ClientState.ChainId) + + require.Empty(t, resultGenesis.InitialValSet) + require.NotEmpty(t, resultGenesis.Provider.InitialValSet) + require.Equal(t, resultGenesis.Params.RetryDelayPeriod, ccvtypes.DefaultRetryDelayPeriod) + + // Check params: retry_delay_period prevents direct comparison + require.EqualValues(t, srcGenesis.Params.Enabled, resultGenesis.Params.Enabled) + require.EqualValues(t, srcGenesis.Params.BlocksPerDistributionTransmission, resultGenesis.Params.BlocksPerDistributionTransmission) + require.EqualValues(t, srcGenesis.Params.DistributionTransmissionChannel, resultGenesis.Params.DistributionTransmissionChannel) + require.EqualValues(t, srcGenesis.Params.ProviderFeePoolAddrStr, resultGenesis.Params.ProviderFeePoolAddrStr) + require.EqualValues(t, srcGenesis.Params.CcvTimeoutPeriod, resultGenesis.Params.CcvTimeoutPeriod) + require.EqualValues(t, srcGenesis.Params.TransferTimeoutPeriod, resultGenesis.Params.TransferTimeoutPeriod) + require.EqualValues(t, srcGenesis.Params.ConsumerRedistributionFraction, resultGenesis.Params.ConsumerRedistributionFraction) + require.EqualValues(t, srcGenesis.Params.HistoricalEntries, resultGenesis.Params.HistoricalEntries) + require.EqualValues(t, srcGenesis.Params.UnbondingPeriod, resultGenesis.Params.UnbondingPeriod) + require.EqualValues(t, srcGenesis.Params.SoftOptOutThreshold, resultGenesis.Params.SoftOptOutThreshold) + require.EqualValues(t, srcGenesis.Params.RewardDenoms, resultGenesis.Params.RewardDenoms) + require.EqualValues(t, srcGenesis.Params.ProviderRewardDenoms, resultGenesis.Params.ProviderRewardDenoms) + + require.Equal(t, srcGenesis.ProviderClientState, resultGenesis.Provider.ClientState) + require.Nil(t, resultGenesis.ProviderClientState) + + require.Equal(t, srcGenesis.Provider.ConsensusState, resultGenesis.ProviderConsensusState) + require.Nil(t, resultGenesis.ProviderConsensusState) + + require.Equal(t, srcGenesis.NewChain, resultGenesis.NewChain) + require.Equal(t, "", resultGenesis.ProviderClientId) + require.Equal(t, "", resultGenesis.ProviderChannelId) + require.Equal(t, srcGenesis.InitialValSet, resultGenesis.Provider.InitialValSet) + require.Empty(t, resultGenesis.InitialValSet) + +} + +// Check transformation of provider v3.3.x implementation to consumer V2 +func TestConsumerGenesisTransformationV330ToV2(t *testing.T) { + version := "v3.3.x" + filePath := createConsumerDataGenesisFile(t, version) defer os.Remove(filePath) - cmd, err := getGenesisTransformCmd() - require.NoError(t, err, "Error setting up transformation command: %s", err) - cmd.SetArgs([]string{filePath}) + var srcGenesis consumerTypes.GenesisState + ctx := getClientCtx() + err := ctx.Codec.UnmarshalJSON([]byte(consumerGenesisStates[version]), &srcGenesis) + require.NoError(t, err) - output := new(bytes.Buffer) - cmd.SetOutput(output) + targetVersion := "v2.x" + result, err := transformConsumerGenesis(filePath, &targetVersion) require.NoError(t, err) - _, err = cmd.ExecuteC() + + resultGenesis := consumerTypes.GenesisState{} + err = ctx.Codec.UnmarshalJSON(result, &resultGenesis) require.NoError(t, err) - var oldConsumerGenesis map[string]interface{} - err = json.Unmarshal([]byte(consumerGenesisStates[version]), &oldConsumerGenesis) - require.NoError(t, err, "Error parsing old version of ccv genesis content for consumer") + require.Equal(t, srcGenesis.Params, resultGenesis.Params) + require.Equal(t, srcGenesis.Provider.ClientState, resultGenesis.ProviderClientState) + require.Equal(t, srcGenesis.Provider.ConsensusState, resultGenesis.ProviderConsensusState) + require.Equal(t, srcGenesis.NewChain, resultGenesis.NewChain) + require.Equal(t, "", resultGenesis.ProviderClientId) + require.Equal(t, "", resultGenesis.ProviderChannelId) - consumerGenesis := consumerTypes.GenesisState{} +} - bz := output.Bytes() - ctx := client.GetClientContextFromCmd(cmd) - err = ctx.Codec.UnmarshalJSON(bz, &consumerGenesis) - require.NoError(t, err, "Error unmarshalling transformed genesis state :%s", err) +// Check transformation of provider v3.3.x implementation to current consumer version +func TestConsumerGenesisTransformationV330ToCurrent(t *testing.T) { + version := "v3.3.x" + filePath := createConsumerDataGenesisFile(t, version) + defer os.Remove(filePath) - // Some basic sanity checks on the content. - require.Nil(t, consumerGenesis.ProviderClientState) - require.NotNil(t, consumerGenesis.Provider.ClientState) - require.Equal(t, "cosmoshub-4", consumerGenesis.Provider.ClientState.ChainId) - - require.Nil(t, consumerGenesis.ProviderConsensusState) - require.NotNil(t, consumerGenesis.Provider.ConsensusState) - require.Equal(t, time.Date(2023, time.May, 8, 11, 0, 1, 563901871, time.UTC), - consumerGenesis.Provider.ConsensusState.Timestamp) - - require.Empty(t, consumerGenesis.InitialValSet) - require.NotEmpty(t, consumerGenesis.Provider.InitialValSet) - require.Equal(t, consumerGenesis.Params.RetryDelayPeriod, ccvtypes.DefaultRetryDelayPeriod) - require.Equal(t, consumerGenesis.NewChain, oldConsumerGenesis["new_chain"]) + var srcGenesis consumerTypes.GenesisState + ctx := getClientCtx() + err := ctx.Codec.UnmarshalJSON([]byte(consumerGenesisStates[version]), &srcGenesis) + require.NoError(t, err) + + result, err := transformConsumerGenesis(filePath, nil) + require.NoError(t, err) + + resultGenesis := consumerTypes.GenesisState{} + err = ctx.Codec.UnmarshalJSON(result, &resultGenesis) + require.NoError(t, err) + + require.Equal(t, srcGenesis.Params.Enabled, resultGenesis.Params.Enabled) + require.Equal(t, srcGenesis.Params.BlocksPerDistributionTransmission, resultGenesis.Params.BlocksPerDistributionTransmission) + require.Equal(t, srcGenesis.Params.DistributionTransmissionChannel, resultGenesis.Params.DistributionTransmissionChannel) + require.Equal(t, srcGenesis.Params.ProviderFeePoolAddrStr, resultGenesis.Params.ProviderFeePoolAddrStr) + require.Equal(t, srcGenesis.Params.CcvTimeoutPeriod, resultGenesis.Params.CcvTimeoutPeriod) + require.Equal(t, srcGenesis.Params.TransferTimeoutPeriod, resultGenesis.Params.TransferTimeoutPeriod) + require.Equal(t, srcGenesis.Params.ConsumerRedistributionFraction, resultGenesis.Params.ConsumerRedistributionFraction) + require.Equal(t, srcGenesis.Params.HistoricalEntries, resultGenesis.Params.HistoricalEntries) + require.Equal(t, srcGenesis.Params.UnbondingPeriod, resultGenesis.Params.UnbondingPeriod) + require.Equal(t, srcGenesis.Params.SoftOptOutThreshold, resultGenesis.Params.SoftOptOutThreshold) + require.Equal(t, srcGenesis.Params.RewardDenoms, resultGenesis.Params.RewardDenoms) + require.Equal(t, srcGenesis.Params.ProviderRewardDenoms, resultGenesis.Params.ProviderRewardDenoms) + + require.Equal(t, resultGenesis.Params.RetryDelayPeriod, ccvtypes.DefaultRetryDelayPeriod) + + require.Equal(t, srcGenesis.Provider.ClientState, resultGenesis.Provider.ClientState) + require.Nil(t, resultGenesis.ProviderClientState) + require.Nil(t, resultGenesis.ProviderConsensusState) + + require.Equal(t, srcGenesis.Provider.ConsensusState, resultGenesis.Provider.ConsensusState) + require.Equal(t, srcGenesis.NewChain, resultGenesis.NewChain) + require.Equal(t, "", resultGenesis.ProviderClientId) + require.Equal(t, "", resultGenesis.ProviderChannelId) +} + +// Check transformation of provider v4.x implementation to consumer V2 +func TestConsumerGenesisTransformationV4ToV2(t *testing.T) { + version := "v4.x" + filePath := createConsumerDataGenesisFile(t, version) + defer os.Remove(filePath) + + var srcGenesis consumerTypes.GenesisState + ctx := getClientCtx() + err := ctx.Codec.UnmarshalJSON([]byte(consumerGenesisStates[version]), &srcGenesis) + require.NoError(t, err) + + targetVersion := "v2.x" + result, err := transformConsumerGenesis(filePath, &targetVersion) + require.NoError(t, err) + + resultGenesis := consumerTypes.GenesisState{} + err = ctx.Codec.UnmarshalJSON(result, &resultGenesis) + require.NoError(t, err) + + // Check params: retry_delay_period prevents direct comparison + require.EqualValues(t, srcGenesis.Params.Enabled, resultGenesis.Params.Enabled) + require.EqualValues(t, srcGenesis.Params.BlocksPerDistributionTransmission, resultGenesis.Params.BlocksPerDistributionTransmission) + require.EqualValues(t, srcGenesis.Params.DistributionTransmissionChannel, resultGenesis.Params.DistributionTransmissionChannel) + require.EqualValues(t, srcGenesis.Params.ProviderFeePoolAddrStr, resultGenesis.Params.ProviderFeePoolAddrStr) + require.EqualValues(t, srcGenesis.Params.CcvTimeoutPeriod, resultGenesis.Params.CcvTimeoutPeriod) + require.EqualValues(t, srcGenesis.Params.TransferTimeoutPeriod, resultGenesis.Params.TransferTimeoutPeriod) + require.EqualValues(t, srcGenesis.Params.ConsumerRedistributionFraction, resultGenesis.Params.ConsumerRedistributionFraction) + require.EqualValues(t, srcGenesis.Params.HistoricalEntries, resultGenesis.Params.HistoricalEntries) + require.EqualValues(t, srcGenesis.Params.UnbondingPeriod, resultGenesis.Params.UnbondingPeriod) + require.EqualValues(t, srcGenesis.Params.SoftOptOutThreshold, resultGenesis.Params.SoftOptOutThreshold) + require.EqualValues(t, srcGenesis.Params.RewardDenoms, resultGenesis.Params.RewardDenoms) + require.EqualValues(t, srcGenesis.Params.ProviderRewardDenoms, resultGenesis.Params.ProviderRewardDenoms) + require.Equal(t, resultGenesis.Params.RetryDelayPeriod, time.Duration(0)) + + require.Equal(t, srcGenesis.Provider.ClientState, resultGenesis.ProviderClientState) + require.Nil(t, resultGenesis.Provider.ClientState) + require.Equal(t, srcGenesis.Provider.ConsensusState, resultGenesis.ProviderConsensusState) + require.Nil(t, resultGenesis.Provider.ConsensusState) + require.Equal(t, "", resultGenesis.ProviderClientId) + require.Equal(t, "", resultGenesis.ProviderChannelId) + + require.Equal(t, 0, len(resultGenesis.Provider.InitialValSet)) + require.Equal(t, srcGenesis.Provider.InitialValSet, resultGenesis.InitialValSet) + require.Empty(t, resultGenesis.Provider.InitialValSet) + + require.Equal(t, srcGenesis.NewChain, resultGenesis.NewChain) +} + +// Check transformation of provider v3.3.x implementation to consumer V2 +func TestConsumerGenesisTransformationV4ToV33(t *testing.T) { + version := "v4.x" + filePath := createConsumerDataGenesisFile(t, version) + defer os.Remove(filePath) + + var srcGenesis ccvtypes.ConsumerGenesisState + ctx := getClientCtx() + err := ctx.Codec.UnmarshalJSON([]byte(consumerGenesisStates[version]), &srcGenesis) + require.NoError(t, err) + + targetVersion := "v3.3.x" + result, err := transformConsumerGenesis(filePath, &targetVersion) + require.NoError(t, err) + resultGenesis := consumerTypes.GenesisState{} //Only difference to v33 is no RetryDelayPeriod + err = ctx.Codec.UnmarshalJSON(result, &resultGenesis) + require.NoError(t, err) + + // Check params: retry_delay_period prevents direct comparison + require.EqualValues(t, srcGenesis.Params.Enabled, resultGenesis.Params.Enabled) + require.EqualValues(t, srcGenesis.Params.BlocksPerDistributionTransmission, resultGenesis.Params.BlocksPerDistributionTransmission) + require.EqualValues(t, srcGenesis.Params.DistributionTransmissionChannel, resultGenesis.Params.DistributionTransmissionChannel) + require.EqualValues(t, srcGenesis.Params.ProviderFeePoolAddrStr, resultGenesis.Params.ProviderFeePoolAddrStr) + require.EqualValues(t, srcGenesis.Params.CcvTimeoutPeriod, resultGenesis.Params.CcvTimeoutPeriod) + require.EqualValues(t, srcGenesis.Params.TransferTimeoutPeriod, resultGenesis.Params.TransferTimeoutPeriod) + require.EqualValues(t, srcGenesis.Params.ConsumerRedistributionFraction, resultGenesis.Params.ConsumerRedistributionFraction) + require.EqualValues(t, srcGenesis.Params.HistoricalEntries, resultGenesis.Params.HistoricalEntries) + require.EqualValues(t, srcGenesis.Params.UnbondingPeriod, resultGenesis.Params.UnbondingPeriod) + require.EqualValues(t, srcGenesis.Params.SoftOptOutThreshold, resultGenesis.Params.SoftOptOutThreshold) + require.EqualValues(t, srcGenesis.Params.RewardDenoms, resultGenesis.Params.RewardDenoms) + require.EqualValues(t, srcGenesis.Params.ProviderRewardDenoms, resultGenesis.Params.ProviderRewardDenoms) + + require.Equal(t, srcGenesis.Provider.ClientState, resultGenesis.Provider.ClientState) + require.Nil(t, resultGenesis.ProviderClientState) + + require.Equal(t, srcGenesis.Provider.ConsensusState, resultGenesis.Provider.ConsensusState) + require.Nil(t, resultGenesis.ProviderConsensusState) + + require.Equal(t, srcGenesis.NewChain, resultGenesis.NewChain) + require.Equal(t, "", resultGenesis.ProviderClientId) + require.Equal(t, "", resultGenesis.ProviderChannelId) + require.Equal(t, srcGenesis.Provider.InitialValSet, resultGenesis.Provider.InitialValSet) + require.Empty(t, resultGenesis.InitialValSet) } diff --git a/docs/docs/consumer-development/consumer-genesis-transformation.md b/docs/docs/consumer-development/consumer-genesis-transformation.md index 2d0ac51e69..3b253fb99d 100644 --- a/docs/docs/consumer-development/consumer-genesis-transformation.md +++ b/docs/docs/consumer-development/consumer-genesis-transformation.md @@ -5,25 +5,36 @@ sidebar_position: 6 # Consumer Genesis Transformation Preparing a consumer chain for onboarding requires some information explaining how to run your chain. This includes a genesis file with CCV data where the CCV data is exported from the provider chain and added to the consumers genesis file (for more details check the documentation on [Onboarding](./onboarding.md) and [Changeover](./changeover-procedure.md)). -In case that the provider chain is running an older version of the InterChainSecurity (ICS) module than the consumer chain the exported CCV data might need to be transformed to the format supported by the ICS implementation run on the consumer chain. This is the case if the consumer chain runs version 4 of ICS or later and the provider is running version 3 or older of the ICS module. +In case that the provider chain is running an older version of the InterChainSecurity (ICS) module than the consumer chain - or vice versa - the exported CCV data might need to be transformed to the format supported by the ICS implementation run on the consumer chain. This is the case if the consumer chain runs version 4 of ICS or later and the provider is running version 3 or older of the ICS module. + +Check the [compatibility notes](../../../RELEASES.md#backwards-compatibility) for known incompatibilities between provider and consumer versions and indications if a consumer genesis transformation is required. To transform such CCV data follow the instructions below ## 1. Prerequisite -- Provider chain is running version 3 or older of the ICS provider module -- Consumer is running version 4 or later of the ICS consumer module. -- interchain-security-cd application complies to the version run on the consumer chain +- used provider and consumer versions require transformation step as indicated in in the [compatibility notes](../../../RELEASES.md#backwards-compatibility) +- interchain-security-cd application supports the versions used by the consumer and provider ## 2. Export the CCV data -Export the CCV data from the provider chain as described in the [Onboarding](./onboarding.md) and [Changeover](./changeover-procedure.md)) your following. +Export the CCV data from the provider chain as described in the [Onboarding](./onboarding.md) and [Changeover](./changeover-procedure.md) your following. As a result the CCV data will be stored in a file in JSON format. ## 3. Transform CCV data -To transform the CCV data to the newer format run the following command. -``` -interchain-security-cd genesis transform [genesis-file] -``` -where 'genesis-file' is the path to the file containing the CCV data exported in [step 2](#2-export-the-ccv-data). -As a result the CCV data in the new format will be written to standard output. - -Use the new CCV data as described in the procedure you're following. \ No newline at end of file +To transform the CCV data +- to the format supported by the current version of the consumer run the following command: + ``` + interchain-security-cd genesis transform [genesis-file] + ``` + where 'genesis-file' is the path to the file containing the CCV data exported in [step 2](#2-export-the-ccv-data). + As a result the CCV data in the new format will be written to standard output. +- a specific target version of a consumer run the following command: + ``` + interchain-security-cd genesis transform --to [genesis-file] + + ``` + where ` ccv-state.json jq -s '.[0].app_state.ccvconsumer = .[1] | .[0]' genesis.json ccv-state.json > ccv.json ``` +Transformation of the exported consumer genesis state to the target version of the consumer might be needed in case the provider and consumer formats are incompatible. +Refer to the compatibility notes [here](../../../RELEASES.md#backwards-compatibility) to check if data transformation is needed for your case. +Instructions on how to transform the exported CCV genesis state (`ccv-state.json` in the example above) to the required target version can be found [here](../consumer-development/consumer-genesis-transformation.md) + ### 2. `SoftwareUpgradeProposal` on the standalone/consumer chain This upgrade proposal will introduce ICS to the standalone chain, making it a consumer. @@ -37,7 +41,7 @@ After `spawn_time`, make sure to assign a consumer key if you intend to use one. Instructions are available [here](../features/key-assignment.md) -### 4. Perform the software ugprade on standalone chain +### 4. Perform the software upgrade on standalone chain Please use instructions provided by the standalone chain team and make sure to reach out if you are facing issues. The upgrade preparation depends on your setup, so please make sure you prepare ahead of time.