diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 31cafc8a77..cec60ee8a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -208,3 +208,29 @@ jobs: if: env.GIT_DIFF run: | make verify-models + + test-interchain: + runs-on: Gaia-Runner-medium + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + check-latest: true + cache: true + cache-dependency-path: go.sum + - uses: technote-space/get-diff-action@v6.1.2 + id: git_diff + with: + PATTERNS: | + **/*.go + go.mod + go.sum + **/go.mod + **/go.sum + **/Makefile + Makefile + - name: interchain tests + if: env.GIT_DIFF + run: | + make test-interchain diff --git a/Makefile b/Makefile index bea52e7257..9aec35674d 100644 --- a/Makefile +++ b/Makefile @@ -53,17 +53,18 @@ test-integration-cov: go test ./tests/integration/... -timeout 30m -coverpkg=./... -coverprofile=integration-profile.out -covermode=atomic # run interchain tests -# we can use PROVIDER_IMAGE_TAG, PROVIDER_IMAGE_NAME, SOUVEREIGN_IMAGE_TAG, and SOUVEREIGN_IMAGE_NAME to run tests with desired docker images, -# including locally built ones that, for example, contain some of our changes that are not yet on the main branch. -# if not provided, default value for PROVIDER_IMAGE_TAG and SOUVEREIGN_IMAGE_TAG is "latest" and for PROVIDER_IMAGE_NAME -# and SOUVEREIGN_IMAGE_NAME is "ghcr.io/cosmos/interchain-security" +# we can use PROVIDER_IMAGE_TAG, PROVIDER_IMAGE_NAME, CONSUMER_IMAGE_TAG, CONSUMER_IMAGE_NAME, SOVEREIGN_IMAGE_TAG, and SOVEREIGN_IMAGE_NAME to run +# tests with desired docker images, including locally built ones that, for example, contain some of our changes that are not yet on the main branch. +# if not provided, default value for image tag is "latest" and for image name is "ghcr.io/cosmos/interchain-security" test-interchain: cd tests/interchain && \ PROVIDER_IMAGE_NAME=$(PROVIDER_IMAGE_NAME) \ PROVIDER_IMAGE_TAG=$(PROVIDER_IMAGE_TAG) \ - SOUVEREIGN_IMAGE_NAME=$(SOUVEREIGN_IMAGE_NAME) \ - SOUVEREIGN_IMAGE_TAG=$(SOUVEREIGN_IMAGE_TAG) \ - go test ./... -timeout 30m + SOVEREIGN_IMAGE_NAME=$(SOVEREIGN_IMAGE_NAME) \ + SOVEREIGN_IMAGE_TAG=$(SOVEREIGN_IMAGE_TAG) \ + CONSUMER_IMAGE_NAME=$(CONSUMER_IMAGE_NAME) \ + CONSUMER_IMAGE_TAG=$(CONSUMER_IMAGE_TAG) \ + go test ./... -timeout 30m -v # run mbt tests test-mbt: diff --git a/TESTING.md b/TESTING.md index a0cea2173b..b27719d8aa 100644 --- a/TESTING.md +++ b/TESTING.md @@ -91,7 +91,9 @@ make sim-full-no-inactive-vals #run interchain tests (running with the latest image ghcr.io/cosmos/interchain-security:latest) make test-interchain # run interchain tests with specific image(e.g. test-image:local) -make test-interchain PROVIDER_IMAGE_NAME=test-image PROVIDER_IMAGE_TAG=local SOUVEREIGN_IMAGE_NAME=test-image SOUVEREIGN_IMAGE_TAG=local +make test-interchain PROVIDER_IMAGE_NAME=test-image PROVIDER_IMAGE_TAG=local SOVEREIGN_IMAGE_NAME=test-image SOVEREIGN_IMAGE_TAG=local +# to run single interchain test, first navigate to /tests/interchain directory and run the command for desired test e.g. +# go test -run ^TestMultiValidatorProviderSuite/TestOptInChainCanOnlyStartIfActiveValidatorOptedIn$ ./... ``` Alternatively you can run tests using `go test`: diff --git a/tests/interchain/chainsuite/chain.go b/tests/interchain/chainsuite/chain.go index 082abac3ed..0df1b97770 100644 --- a/tests/interchain/chainsuite/chain.go +++ b/tests/interchain/chainsuite/chain.go @@ -32,6 +32,8 @@ type Chain struct { ValidatorWallets []ValidatorWallet RelayerWallet ibc.Wallet TestWallets []ibc.Wallet + walletMtx sync.Mutex + walletsInUse map[int]bool } type ValidatorWallet struct { @@ -49,6 +51,7 @@ func chainFromCosmosChain(cosmos *cosmos.CosmosChain, relayerWallet ibc.Wallet, c.ValidatorWallets = wallets c.RelayerWallet = relayerWallet c.TestWallets = testWallets + c.walletsInUse = make(map[int]bool) return c, nil } @@ -88,7 +91,7 @@ func CreateChain(ctx context.Context, testName interchaintest.TestName, spec *in } // build test wallets - testWallets, err := setupTestWallets(ctx, cosmosChain, TestWalletsNumber) + testWallets, err := SetupTestWallets(ctx, cosmosChain, TestWalletsNumber) if err != nil { return nil, err } @@ -100,7 +103,7 @@ func CreateChain(ctx context.Context, testName interchaintest.TestName, spec *in return chain, nil } -func setupTestWallets(ctx context.Context, cosmosChain *cosmos.CosmosChain, walletCount int) ([]ibc.Wallet, error) { +func SetupTestWallets(ctx context.Context, cosmosChain *cosmos.CosmosChain, walletCount int) ([]ibc.Wallet, error) { wallets := make([]ibc.Wallet, walletCount) eg := new(errgroup.Group) for i := 0; i < walletCount; i++ { @@ -159,6 +162,85 @@ func getValidatorWallets(ctx context.Context, chain *Chain) ([]ValidatorWallet, return wallets, nil } +func (p *Chain) AddConsumerChain(ctx context.Context, relayer *Relayer, spec *interchaintest.ChainSpec) (*Chain, error) { + dockerClient, dockerNetwork := GetDockerContext(ctx) + + cf := interchaintest.NewBuiltinChainFactory( + GetLogger(ctx), + []*interchaintest.ChainSpec{spec}, + ) + + chains, err := cf.Chains(p.GetNode().TestName) + if err != nil { + return nil, err + } + consumer := chains[0].(*cosmos.CosmosChain) + + // We can't use AddProviderConsumerLink here because the provider chain is already built; we'll have to do everything by hand. + p.Consumers = append(p.Consumers, consumer) + consumer.Provider = p.CosmosChain + relayerWallet, err := consumer.BuildRelayerWallet(ctx, "relayer-"+consumer.Config().ChainID) + if err != nil { + return nil, err + } + wallets := make([]ibc.Wallet, len(p.Validators)+1) + wallets[0] = relayerWallet + // This is a hack, but we need to create wallets for the validators that have the right moniker. + for i := 1; i <= len(p.Validators); i++ { + wallets[i], err = consumer.BuildRelayerWallet(ctx, ValidatorMoniker) + if err != nil { + return nil, err + } + } + walletAmounts := make([]ibc.WalletAmount, len(wallets)) + for i, wallet := range wallets { + walletAmounts[i] = ibc.WalletAmount{ + Address: wallet.FormattedAddress(), + Denom: consumer.Config().Denom, + Amount: sdkmath.NewInt(TotalValidatorFunds), + } + } + + ic := interchaintest.NewInterchain(). + AddChain(consumer, walletAmounts...). + AddRelayer(relayer, "relayer") + + if err := ic.Build(ctx, GetRelayerExecReporter(ctx), interchaintest.InterchainBuildOptions{ + Client: dockerClient, + NetworkID: dockerNetwork, + TestName: p.GetNode().TestName, + }); err != nil { + return nil, err + } + + for i, val := range consumer.Validators { + if err := val.RecoverKey(ctx, ValidatorMoniker, wallets[i+1].Mnemonic()); err != nil { + return nil, err + } + } + consumerChain, err := chainFromCosmosChain(consumer, relayerWallet, p.TestWallets) + if err != nil { + return nil, err + } + + return consumerChain, nil +} + +// GetUnusedTestingAddresss retrieves an unused wallet address and its key name safely +func (p *Chain) GetUnusedTestingAddresss() (formattedAddress string, keyName string, err error) { + p.walletMtx.Lock() + defer p.walletMtx.Unlock() + + for i, wallet := range p.TestWallets { + if !p.walletsInUse[i] { + p.walletsInUse[i] = true + return wallet.FormattedAddress(), wallet.KeyName(), nil + } + } + + return "", "", fmt.Errorf("no unused wallets available") +} + // UpdateAndVerifyStakeChange updates the staking amount on the provider chain and verifies that the change is reflected on the consumer side func (p *Chain) UpdateAndVerifyStakeChange(ctx context.Context, consumer *Chain, relayer *Relayer, amount, valIdx int) error { @@ -552,6 +634,36 @@ func (c *Chain) GetCcvConsumerParams(ctx context.Context) (ConsumerParamsRespons return queryResponse, nil } +func (c *Chain) GetProviderInfo(ctx context.Context) (ProviderInfoResponse, error) { + queryRes, _, err := c.GetNode().ExecQuery( + ctx, + "ccvconsumer", "provider-info", + ) + if err != nil { + return ProviderInfoResponse{}, err + } + + var queryResponse ProviderInfoResponse + err = json.Unmarshal([]byte(queryRes), &queryResponse) + if err != nil { + return ProviderInfoResponse{}, err + } + + return queryResponse, nil +} + +func (c *Chain) QueryJSON(ctx context.Context, jsonPath string, query ...string) (gjson.Result, error) { + stdout, _, err := c.GetNode().ExecQuery(ctx, query...) + if err != nil { + return gjson.Result{}, err + } + retval := gjson.GetBytes(stdout, jsonPath) + if !retval.Exists() { + return gjson.Result{}, fmt.Errorf("json path %s not found in query result %s", jsonPath, stdout) + } + return retval, nil +} + func getEvtAttribute(events []abci.Event, evtType string, key string) (string, bool) { for _, evt := range events { if evt.GetType() == evtType { diff --git a/tests/interchain/chainsuite/chain_spec_consumer.go b/tests/interchain/chainsuite/chain_spec_consumer.go new file mode 100644 index 0000000000..2f0a6bc873 --- /dev/null +++ b/tests/interchain/chainsuite/chain_spec_consumer.go @@ -0,0 +1,103 @@ +package chainsuite + +import ( + "context" + "errors" + "strconv" + "time" + + providertypes "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/testutil" +) + +func GetConsumerSpec(ctx context.Context, providerChain *Chain, proposalMsg *providertypes.MsgCreateConsumer) *interchaintest.ChainSpec { + fullNodes := FullNodeCount + validators := 1 + + return &interchaintest.ChainSpec{ + ChainName: ConsumerChainID, + NumFullNodes: &fullNodes, + NumValidators: &validators, + ChainConfig: ibc.ChainConfig{ + ChainID: ConsumerChainID, + Bin: ConsumerBin, + Denom: Stake, + Type: CosmosChainType, + GasPrices: GasPrices + Stake, + GasAdjustment: 2.0, + TrustingPeriod: "336h", + CoinType: "118", + Images: []ibc.DockerImage{ + { + Repository: ConsumerImageName(), + Version: ConsumerImageVersion(), + UIDGID: "1025:1025", + }, + }, + ConfigFileOverrides: map[string]any{ + "config/config.toml": DefaultConfigToml(), + }, + PreGenesis: func(consumer ibc.Chain) error { + // note that if Top_N>0 proposal will be rejected. If there is a need to support this in the future, + // it is necessary to first submit a create message with Top_N=0 and then an update message with Top_N>0 + consumerID, err := providerChain.CreateConsumer(ctx, proposalMsg, ValidatorMoniker) + if err != nil { + return err + } + + for index := 0; index < len(providerChain.Validators); index++ { + if err := providerChain.OptIn(ctx, consumerID, index); err != nil { + return err + } + } + + // speed up chain launch by submitting update msg with current time as a spawn time + proposalMsg.InitializationParameters.SpawnTime = time.Now() + updateMsg := &providertypes.MsgUpdateConsumer{ + ConsumerId: consumerID, + Owner: providerChain.ValidatorWallets[0].Address, + NewOwnerAddress: providerChain.ValidatorWallets[0].Address, + InitializationParameters: proposalMsg.InitializationParameters, + PowerShapingParameters: proposalMsg.PowerShapingParameters, + } + if err := providerChain.UpdateConsumer(ctx, updateMsg, ValidatorMoniker); err != nil { + return err + } + + if err := testutil.WaitForBlocks(ctx, 2, providerChain); err != nil { + return err + } + + consumerChain, err := providerChain.GetConsumerChainByChainId(ctx, proposalMsg.ChainId) + if err != nil { + return err + } + + if consumerChain.Phase != providertypes.CONSUMER_PHASE_LAUNCHED.String() { + return errors.New("consumer chain is not launched") + } + + return nil + }, + Bech32Prefix: Bech32PrefixConsumer, + ModifyGenesisAmounts: DefaultGenesisAmounts(Stake), + ModifyGenesis: cosmos.ModifyGenesis(consumerModifiedGenesis()), + InterchainSecurityConfig: ibc.ICSConfig{ + ConsumerCopyProviderKey: func(int) bool { return true }, + }, + }, + } +} + +func consumerModifiedGenesis() []cosmos.GenesisKV { + return []cosmos.GenesisKV{ + cosmos.NewGenesisKV("app_state.slashing.params.signed_blocks_window", strconv.Itoa(SlashingWindowConsumer)), + cosmos.NewGenesisKV("consensus.params.block.max_gas", "50000000"), + cosmos.NewGenesisKV("app_state.ccvconsumer.params.soft_opt_out_threshold", "0.0"), + cosmos.NewGenesisKV("app_state.ccvconsumer.params.blocks_per_distribution_transmission", BlocksPerDistribution), + cosmos.NewGenesisKV("app_state.ccvconsumer.params.reward_denoms", []string{Stake}), + } +} diff --git a/tests/interchain/chainsuite/config.go b/tests/interchain/chainsuite/config.go index 7988937996..ea42399ec7 100644 --- a/tests/interchain/chainsuite/config.go +++ b/tests/interchain/chainsuite/config.go @@ -12,11 +12,12 @@ import ( const ( ProviderBin = "interchain-security-pd" - ProviderBech32Prefix = "cosmos" - ProviderValOperPrefix = "cosmosvaloper" + SovereignBin = "interchain-security-sd" + ConsumerBin = "interchain-security-cdd" ProviderChainID = "ics-provider" SovereignToConsumerChainID = "ics-sovereign-consumer" - SovereignBin = "interchain-security-sd" + ConsumerChainID = "ics-consumer" + ProviderBech32Prefix = "cosmos" Bech32PrefixConsumer = "consumer" Stake = "stake" DowntimeJailDuration = 10 * time.Second @@ -36,7 +37,7 @@ const ( TotalValidatorFunds = 11_000_000_000 ValidatorFunds = 30_000_000 FullNodeCount = 0 - ChainSpawnWait = 155 * time.Second + BlocksPerDistribution = 5 CosmosChainType = "cosmos" ProviderGovModuleAddress = "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn" ConsumerGovModuleAddress = "consumer10d07y265gmmuvt4z0w9aw880jnsr700jlh7295" @@ -67,37 +68,43 @@ func DefaultGenesisAmounts(denom string) func(i int) (sdktypes.Coin, sdktypes.Co } func ProviderImageVersion() string { - providerImageVersion := os.Getenv("PROVIDER_IMAGE_TAG") - if providerImageVersion == "" { - providerImageVersion = "latest" - } - - return providerImageVersion + return getImageVersion("PROVIDER_IMAGE_TAG") } func ProviderImageName() string { - providerImageName := os.Getenv("PROVIDER_IMAGE_NAME") - if providerImageName == "" { - providerImageName = "ghcr.io/cosmos/interchain-security" - } - - return providerImageName + return getImageName("PROVIDER_IMAGE_NAME") } func SouvereignImageVersion() string { - souvereignImageVersion := os.Getenv("SOUVEREIGN_IMAGE_TAG") - if souvereignImageVersion == "" { - souvereignImageVersion = "latest" + return getImageVersion("SOVEREIGN_IMAGE_TAG") +} + +func SouvereignImageName() string { + return getImageName("SOVEREIGN_IMAGE_NAME") +} + +func ConsumerImageVersion() string { + return getImageVersion("CONSUMER_IMAGE_TAG") +} + +func ConsumerImageName() string { + return getImageName("CONSUMER_IMAGE_NAME") +} + +func getImageName(imgName string) string { + imageName := os.Getenv(imgName) + if imageName == "" { + imageName = "ghcr.io/cosmos/interchain-security" } - return souvereignImageVersion + return imageName } -func SouvereignImageName() string { - souvereignImageName := os.Getenv("SOUVEREIGN_IMAGE_NAME") - if souvereignImageName == "" { - souvereignImageName = "ghcr.io/cosmos/interchain-security" +func getImageVersion(imgVersion string) string { + imageVersion := os.Getenv(imgVersion) + if imageVersion == "" { + imageVersion = "latest" } - return souvereignImageName + return imageVersion } diff --git a/tests/interchain/chainsuite/query_types.go b/tests/interchain/chainsuite/query_types.go index fd19028cc8..09c98da563 100644 --- a/tests/interchain/chainsuite/query_types.go +++ b/tests/interchain/chainsuite/query_types.go @@ -209,3 +209,15 @@ type ConsumerParams struct { RetryDelayPeriod string `json:"retry_delay_period"` ConsumerID string `json:"consumer_id"` } + +type ProviderInfoResponse struct { + Consumer ChainDetails `json:"consumer"` + Provider ChainDetails `json:"provider"` +} + +type ChainDetails struct { + ChainID string `json:"chainID"` + ClientID string `json:"clientID"` + ConnectionID string `json:"connectionID"` + ChannelID string `json:"channelID"` +} diff --git a/tests/interchain/changeover_suite.go b/tests/interchain/changeover_suite.go deleted file mode 100644 index 3419905eab..0000000000 --- a/tests/interchain/changeover_suite.go +++ /dev/null @@ -1,67 +0,0 @@ -package interchain - -import ( - "context" - "cosmos/interchain-security/tests/interchain/chainsuite" - "strconv" - - "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" - "github.com/stretchr/testify/suite" -) - -type ChangeoverSuite struct { - suite.Suite - Provider *chainsuite.Chain - Consumer *chainsuite.Chain - Relayer *chainsuite.Relayer - ctx context.Context -} - -func (s *ChangeoverSuite) SetupSuite() { - ctx, err := chainsuite.NewSuiteContext(&s.Suite) - s.Require().NoError(err) - s.ctx = ctx - - // create and start provider chain - s.Provider, err = chainsuite.CreateChain(s.GetContext(), s.T(), chainsuite.GetProviderSpec(1, provChangeoverModifiedGenesis())) - s.Require().NoError(err) - - // create and start sovereign chain that will later changeover to consumer - s.Consumer, err = chainsuite.CreateChain(s.GetContext(), s.T(), chainsuite.GetSovereignSpec()) - s.Require().NoError(err) - - // setup hermes relayer - relayer, err := chainsuite.NewRelayer(s.GetContext(), s.T()) - s.Require().NoError(err) - s.Relayer = relayer - - err = relayer.SetupChainKeys(s.GetContext(), s.Provider) - s.Require().NoError(err) - s.Require().NoError(relayer.RestartRelayer(ctx)) - - err = relayer.SetupChainKeys(s.GetContext(), s.Consumer) - s.Require().NoError(err) - s.Require().NoError(relayer.RestartRelayer(ctx)) -} - -func (s *ChangeoverSuite) GetContext() context.Context { - s.Require().NotNil(s.ctx, "Tried to GetContext before it was set. SetupSuite must run first") - return s.ctx -} - -func provChangeoverModifiedGenesis() []cosmos.GenesisKV { - return []cosmos.GenesisKV{ - cosmos.NewGenesisKV("app_state.staking.params.unbonding_time", (chainsuite.ProviderUnbondingTime * 10000000).String()), - cosmos.NewGenesisKV("app_state.gov.params.voting_period", chainsuite.GovVotingPeriod.String()), - cosmos.NewGenesisKV("app_state.gov.params.max_deposit_period", chainsuite.GovDepositPeriod.String()), - cosmos.NewGenesisKV("app_state.gov.params.min_deposit.0.denom", chainsuite.Stake), - cosmos.NewGenesisKV("app_state.gov.params.min_deposit.0.amount", strconv.Itoa(chainsuite.GovMinDepositAmount)), - cosmos.NewGenesisKV("app_state.slashing.params.signed_blocks_window", strconv.Itoa(chainsuite.ProviderSlashingWindow)), - cosmos.NewGenesisKV("app_state.slashing.params.downtime_jail_duration", chainsuite.DowntimeJailDuration.String()), - cosmos.NewGenesisKV("app_state.slashing.params.slash_fraction_double_sign", chainsuite.SlashFractionDoubleSign), - cosmos.NewGenesisKV("app_state.provider.params.slash_meter_replenish_period", chainsuite.ProviderReplenishPeriod), - cosmos.NewGenesisKV("app_state.provider.params.slash_meter_replenish_fraction", chainsuite.ProviderReplenishFraction), - cosmos.NewGenesisKV("app_state.provider.params.blocks_per_epoch", "1"), - cosmos.NewGenesisKV("app_state.staking.params.max_validators", "1"), - } -} diff --git a/tests/interchain/provider_consumers_suite.go b/tests/interchain/provider_consumers_suite.go new file mode 100644 index 0000000000..f5ccb0370d --- /dev/null +++ b/tests/interchain/provider_consumers_suite.go @@ -0,0 +1,98 @@ +package interchain + +import ( + "context" + "cosmos/interchain-security/tests/interchain/chainsuite" + "strconv" + "time" + + sdkmath "cosmossdk.io/math" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/stretchr/testify/suite" +) + +type ProviderConsumersSuite struct { + suite.Suite + Provider *chainsuite.Chain + Sovereign *chainsuite.Chain + Consumer *chainsuite.Chain + Relayer *chainsuite.Relayer + ctx context.Context +} + +func (s *ProviderConsumersSuite) SetupSuite() { + ctx, err := chainsuite.NewSuiteContext(&s.Suite) + s.Require().NoError(err) + s.ctx = ctx + + // create and start provider chain + s.Provider, err = chainsuite.CreateChain(s.GetContext(), s.T(), chainsuite.GetProviderSpec(1, provConsumerModifiedGenesis())) + s.Require().NoError(err) + + // create and start sovereign chain that will later changeover to consumer + s.Sovereign, err = chainsuite.CreateChain(s.GetContext(), s.T(), chainsuite.GetSovereignSpec()) + s.Require().NoError(err) + + // setup hermes relayer + relayer, err := chainsuite.NewRelayer(s.GetContext(), s.T()) + s.Require().NoError(err) + s.Relayer = relayer + + // create and start consumer chain + spawnTime := time.Now().Add(time.Hour) + initParams := consumerInitParamsTemplate(&spawnTime) + initParams.InitialHeight = clienttypes.Height{RevisionNumber: clienttypes.ParseChainID(chainsuite.ConsumerChainID), RevisionHeight: 1} + proposalMsg := msgCreateConsumer(chainsuite.ConsumerChainID, initParams, powerShapingParamsTemplate(), nil, chainsuite.ProviderGovModuleAddress) + s.Consumer, err = s.Provider.AddConsumerChain(s.GetContext(), relayer, chainsuite.GetConsumerSpec(s.GetContext(), s.Provider, proposalMsg)) + s.Require().NoError(err) + + s.Require().NoError(relayer.SetupChainKeys(s.GetContext(), s.Provider)) + s.Require().NoError(relayer.SetupChainKeys(s.GetContext(), s.Sovereign)) + s.Require().NoError(relayer.SetupChainKeys(s.GetContext(), s.Consumer)) + s.Require().NoError(relayer.RestartRelayer(s.GetContext())) + + // confirm that tx on consumer can not be send before consumer and provider are connected + err = s.Consumer.SendFunds(ctx, chainsuite.ValidatorMoniker, ibc.WalletAmount{ + Amount: sdkmath.NewInt(1000), + Denom: s.Consumer.Config().Denom, + Address: s.Consumer.RelayerWallet.FormattedAddress(), + }) + s.Require().Error(err) + s.Require().Contains(err.Error(), "tx contains unsupported message types") + // connect consumer and provider + s.Require().NoError(s.Relayer.ConnectProviderConsumer(s.GetContext(), s.Provider, s.Consumer)) + s.Require().NoError(relayer.RestartRelayer(s.GetContext())) + s.Require().NoError(s.Provider.UpdateAndVerifyStakeChange(s.GetContext(), s.Consumer, s.Relayer, 1_000_000, 0)) + providerInfo, err := s.Consumer.GetProviderInfo(s.GetContext()) + s.Require().NoError(err) + s.Require().Equal("connection-0", providerInfo.Provider.ConnectionID) + + // build test wallets for consumer after ics connection is established and bank send txs are allowed + testWallets, err := chainsuite.SetupTestWallets(ctx, s.Consumer.CosmosChain, chainsuite.TestWalletsNumber) + s.Require().NoError(err) + s.Consumer.TestWallets = testWallets +} + +func (s *ProviderConsumersSuite) GetContext() context.Context { + s.Require().NotNil(s.ctx, "Tried to GetContext before it was set. SetupSuite must run first") + return s.ctx +} + +func provConsumerModifiedGenesis() []cosmos.GenesisKV { + return []cosmos.GenesisKV{ + cosmos.NewGenesisKV("app_state.staking.params.unbonding_time", (chainsuite.ProviderUnbondingTime * 10000000).String()), + cosmos.NewGenesisKV("app_state.gov.params.voting_period", chainsuite.GovVotingPeriod.String()), + cosmos.NewGenesisKV("app_state.gov.params.max_deposit_period", chainsuite.GovDepositPeriod.String()), + cosmos.NewGenesisKV("app_state.gov.params.min_deposit.0.denom", chainsuite.Stake), + cosmos.NewGenesisKV("app_state.gov.params.min_deposit.0.amount", strconv.Itoa(chainsuite.GovMinDepositAmount)), + cosmos.NewGenesisKV("app_state.slashing.params.signed_blocks_window", strconv.Itoa(chainsuite.ProviderSlashingWindow)), + cosmos.NewGenesisKV("app_state.slashing.params.downtime_jail_duration", chainsuite.DowntimeJailDuration.String()), + cosmos.NewGenesisKV("app_state.slashing.params.slash_fraction_double_sign", chainsuite.SlashFractionDoubleSign), + cosmos.NewGenesisKV("app_state.provider.params.slash_meter_replenish_period", chainsuite.ProviderReplenishPeriod), + cosmos.NewGenesisKV("app_state.provider.params.slash_meter_replenish_fraction", chainsuite.ProviderReplenishFraction), + cosmos.NewGenesisKV("app_state.provider.params.blocks_per_epoch", "1"), + cosmos.NewGenesisKV("app_state.staking.params.max_validators", "1"), + } +} diff --git a/tests/interchain/changeover_test.go b/tests/interchain/provider_consumers_test.go similarity index 55% rename from tests/interchain/changeover_test.go rename to tests/interchain/provider_consumers_test.go index f1f4941ed2..c8355dd126 100644 --- a/tests/interchain/changeover_test.go +++ b/tests/interchain/provider_consumers_test.go @@ -7,45 +7,49 @@ import ( "testing" "time" + sdkmath "cosmossdk.io/math" upgradetypes "cosmossdk.io/x/upgrade/types" govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" providertypes "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" + "github.com/strangelove-ventures/interchaintest/v8" "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" "github.com/strangelove-ventures/interchaintest/v8/testutil" "github.com/stretchr/testify/suite" "github.com/tidwall/sjson" "golang.org/x/sync/errgroup" ) -func TestChangeoverSuite(t *testing.T) { - s := &ChangeoverSuite{} +func TestProviderConsumersSuite(t *testing.T) { + s := &ProviderConsumersSuite{} suite.Run(t, s) } -func (s *ChangeoverSuite) TestSovereignToConsumer() { +func (s *ProviderConsumersSuite) TestSovereignToConsumerChangeover() { // submit MsgCreateConsumer and verify that the chain is in launched phase - currentHeight, err := s.Consumer.Height(s.GetContext()) + currentHeight, err := s.Sovereign.Height(s.GetContext()) s.Require().NoError(err) spawnTime := time.Now().Add(time.Hour) initializationParams := consumerInitParamsTemplate(&spawnTime) initialHeight := uint64(currentHeight) + 60 initializationParams.InitialHeight = clienttypes.Height{ - RevisionNumber: clienttypes.ParseChainID(s.Consumer.Config().ChainID), + RevisionNumber: clienttypes.ParseChainID(s.Sovereign.Config().ChainID), RevisionHeight: initialHeight, } powerShapingParams := powerShapingParamsTemplate() - createConsumerMsg := msgCreateConsumer(s.Consumer.Config().ChainID, initializationParams, powerShapingParams, nil, s.Provider.ValidatorWallets[0].Address) + createConsumerMsg := msgCreateConsumer(s.Sovereign.Config().ChainID, initializationParams, powerShapingParams, nil, s.Provider.ValidatorWallets[0].Address) consumerID, err := s.Provider.CreateConsumer(s.GetContext(), createConsumerMsg, chainsuite.ValidatorMoniker) s.Require().NoError(err) // opt in validator s.Require().NoError(s.Provider.OptIn(s.GetContext(), consumerID, 0)) // assign consumer key - valConsumerKey, err := s.Consumer.GetValidatorKey(s.GetContext(), 0) + valConsumerKey, err := s.Sovereign.GetValidatorKey(s.GetContext(), 0) s.Require().NoError(err) s.Require().NoError(s.Provider.AssignKey(s.GetContext(), consumerID, 0, valConsumerKey)) // update spawn time @@ -71,20 +75,20 @@ func (s *ChangeoverSuite) TestSovereignToConsumer() { Height: int64(initialHeight) - 3, }, } - s.Require().NoError(s.Consumer.ExecuteProposalMsg(s.GetContext(), upgradeMsg, chainsuite.ConsumerGovModuleAddress, "Changeover", cosmos.ProposalVoteYes, govv1.StatusPassed, false)) + s.Require().NoError(s.Sovereign.ExecuteProposalMsg(s.GetContext(), upgradeMsg, chainsuite.ConsumerGovModuleAddress, "Changeover", cosmos.ProposalVoteYes, govv1.StatusPassed, false)) // wait for sw upgrade height - currentHeight, err = s.Consumer.Height(s.GetContext()) + currentHeight, err = s.Sovereign.Height(s.GetContext()) s.Require().NoError(err) timeoutCtx, timeoutCtxCancel := context.WithTimeout(s.GetContext(), (time.Duration(int64(initialHeight)-currentHeight)+10)*chainsuite.CommitTimeout) defer timeoutCtxCancel() - err = testutil.WaitForBlocks(timeoutCtx, int(int64(initialHeight)-currentHeight)+3, s.Consumer) + err = testutil.WaitForBlocks(timeoutCtx, int(int64(initialHeight)-currentHeight)+3, s.Sovereign) s.Require().Error(err) // stop sovereign chain - s.Require().NoError(s.Consumer.StopAllNodes(s.GetContext())) + s.Require().NoError(s.Sovereign.StopAllNodes(s.GetContext())) - genesis, err := s.Consumer.GetNode().GenesisFileContent(s.GetContext()) + genesis, err := s.Sovereign.GetNode().GenesisFileContent(s.GetContext()) s.Require().NoError(err) // insert consumer genesis section @@ -93,8 +97,11 @@ func (s *ChangeoverSuite) TestSovereignToConsumer() { genesis, err = sjson.SetRawBytes(genesis, "app_state.ccvconsumer", ccvState) s.Require().NoError(err) + genesis, err = sjson.SetBytes(genesis, "app_state.ccvconsumer.preCCV", true) + s.Require().NoError(err) + eg := errgroup.Group{} - for _, val := range s.Consumer.Validators { + for _, val := range s.Sovereign.Validators { val := val eg.Go(func() error { if err := val.OverwriteGenesisFile(s.GetContext(), []byte(genesis)); err != nil { @@ -106,14 +113,47 @@ func (s *ChangeoverSuite) TestSovereignToConsumer() { s.Require().NoError(eg.Wait()) // replace the binary and restart consumer node - s.Consumer.ChangeBinary(s.GetContext(), "interchain-security-cdd") - s.Require().NoError(s.Consumer.StartAllNodes(s.GetContext())) - s.Require().NoError(s.Relayer.ConnectProviderConsumer(s.GetContext(), s.Provider, s.Consumer)) + s.Sovereign.ChangeBinary(s.GetContext(), chainsuite.ConsumerBin) + s.Require().NoError(s.Sovereign.StartAllNodes(s.GetContext())) + s.Require().NoError(s.Relayer.ConnectProviderConsumer(s.GetContext(), s.Provider, s.Sovereign)) s.Require().NoError(s.Relayer.StopRelayer(s.GetContext(), chainsuite.GetRelayerExecReporter(s.GetContext()))) s.Require().NoError(s.Relayer.StartRelayer(s.GetContext(), chainsuite.GetRelayerExecReporter(s.GetContext()))) - params, err := s.Consumer.GetCcvConsumerParams(s.ctx) + params, err := s.Sovereign.GetCcvConsumerParams(s.ctx) s.Require().NoError(err) s.Require().True(params.Params.HistoricalEntries == fmt.Sprint(initializationParams.HistoricalEntries)) // check if consumer is connected and functional - s.Require().NoError(s.Provider.UpdateAndVerifyStakeChange(s.GetContext(), s.Consumer, s.Relayer, 1_000_000, 0)) + s.Require().NoError(s.Provider.UpdateAndVerifyStakeChange(s.GetContext(), s.Sovereign, s.Relayer, 1_000_000, 0)) +} + +func (s *ProviderConsumersSuite) TestRewards() { + transferCh, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Provider, s.Consumer) + s.Require().NoError(err) + s.Require().True(transferCh != nil) + rewardDenom := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom("transfer", transferCh.ChannelID, s.Consumer.Config().Denom)).IBCDenom() + + govAuthority, err := s.Provider.GetGovernanceAddress(s.GetContext()) + s.Require().NoError(err) + rewardDenomsProp := &providertypes.MsgChangeRewardDenoms{ + DenomsToAdd: []string{rewardDenom}, + Authority: govAuthority, + } + s.Require().NoError(s.Provider.ExecuteProposalMsg(s.GetContext(), rewardDenomsProp, chainsuite.ProviderGovModuleAddress, "change reward denoms", cosmos.ProposalVoteYes, govv1.StatusPassed, false)) + + s.Require().NoError(s.Consumer.SendFunds(s.GetContext(), interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Amount: sdkmath.NewInt(10000), + Denom: s.Consumer.Config().Denom, + Address: s.Consumer.ValidatorWallets[0].Address, + })) + + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), chainsuite.BlocksPerDistribution+2, s.Provider, s.Consumer)) + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 2, s.Provider, s.Consumer)) + + rewardStr, err := s.Provider.QueryJSON( + s.GetContext(), fmt.Sprintf("total.#(%%\"*%s\")", rewardDenom), + "distribution", "rewards", s.Provider.ValidatorWallets[0].Address, + ) + s.Require().NoError(err) + rewards, err := StrToSDKInt(rewardStr.String()) + s.Require().NoError(err) + s.Require().True(rewards.GT(sdkmath.NewInt(0)), "rewards: %s", rewards.String()) } diff --git a/tests/interchain/provider_single_val_test.go b/tests/interchain/provider_single_val_test.go index 82434f81f0..053796d624 100644 --- a/tests/interchain/provider_single_val_test.go +++ b/tests/interchain/provider_single_val_test.go @@ -35,7 +35,7 @@ func TestSingleProviderSuite(t *testing.T) { // Confirm that a chain can be created with initialization parameters that do not contain a spawn time // Confirm that if there are no opted-in validators at spawn time, the chain fails to launch and moves back to its Registered phase having reset its spawn time func (s *SingleValidatorProviderSuite) TestProviderCreateConsumer() { - testAcc, testAccKey, err := s.GetUnusedTestingAddresss() + testAcc, testAccKey, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) // Confirm that a chain can be created with the minimum params (metadata) @@ -89,7 +89,7 @@ func (s *SingleValidatorProviderSuite) TestProviderCreateConsumer() { // Confirm that a chain without the minimum params (metadata) is rejected // Confirm that a chain voted 'no' is rejected func (s *SingleValidatorProviderSuite) TestProviderCreateConsumerRejection() { - testAcc, testAccKey, err := s.GetUnusedTestingAddresss() + testAcc, testAccKey, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) chainName := "rejectConsumer-1" @@ -111,7 +111,7 @@ func (s *SingleValidatorProviderSuite) TestProviderCreateConsumerRejection() { // Scenario 1: Validators opted in, MsgUpdateConsumer called to set spawn time in the past -> chain should start. // Scenario 2: Validators opted in, spawn time is in the future, the chain starts after the spawn time. func (s *SingleValidatorProviderSuite) TestProviderValidatorOptIn() { - testAcc, testAccKey, err := s.GetUnusedTestingAddresss() + testAcc, testAccKey, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) // Scenario 1: Validators opted in, MsgUpdateConsumer called to set spawn time in the past -> chain should start. @@ -165,7 +165,7 @@ func (s *SingleValidatorProviderSuite) TestProviderValidatorOptIn() { // -> Check that consumer chain genesis is available and contains the correct validator key // If possible, confirm that a validator can change their key assignment (from hub key to consumer chain key and/or vice versa) func (s *SingleValidatorProviderSuite) TestProviderValidatorOptInWithKeyAssignment() { - testAcc, testAccKey, err := s.GetUnusedTestingAddresss() + testAcc, testAccKey, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) valConsumerKeyVal := "Ui5Gf1+mtWUdH8u3xlmzdKID+F3PK0sfXZ73GZ6q6is=" @@ -232,7 +232,7 @@ func (s *SingleValidatorProviderSuite) TestProviderValidatorOptInWithKeyAssignme // If there are no opted-in validators and the spawn time is in the past, the chain should not start. // Confirm that a chain remains in the Registered phase unless all the initialization parameters are set for it func (s *SingleValidatorProviderSuite) TestProviderUpdateConsumer() { - testAcc, testAccKey, err := s.GetUnusedTestingAddresss() + testAcc, testAccKey, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) chainName := "updateConsumer-1" @@ -294,7 +294,7 @@ func (s *SingleValidatorProviderSuite) TestProviderUpdateConsumer() { // Confirm that the chain can be updated to a higher TopN // Confirm that the owner of the chain cannot change as long as it remains a Top N chain func (s *SingleValidatorProviderSuite) TestProviderTransformOptInToTopN() { - testAcc, testAccKey, err := s.GetUnusedTestingAddresss() + testAcc, testAccKey, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) // Create an opt-in chain, owner is testAcc1 @@ -367,7 +367,7 @@ func (s *SingleValidatorProviderSuite) TestProviderTransformOptInToTopN() { // Create a Top N chain, and transform it to an opt-in via `tx gov submit-proposal` using MsgUpdateConsumer // Confirm that the chain is now not owned by governance func (s *SingleValidatorProviderSuite) TestProviderTransformTopNtoOptIn() { - testAcc, _, err := s.GetUnusedTestingAddresss() + testAcc, _, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) chainName := "transformTopNtoOptIn-1" @@ -411,7 +411,7 @@ func (s *SingleValidatorProviderSuite) TestProviderTransformTopNtoOptIn() { // TestOptOut tests removing validator from consumer-opted-in-validators func (s *SingleValidatorProviderSuite) TestOptOut() { - testAcc, testAccKey, err := s.GetUnusedTestingAddresss() + testAcc, testAccKey, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) // Add consumer chain @@ -460,7 +460,7 @@ func (s *SingleValidatorProviderSuite) TestOptOut() { // Confirm that after unbonding period, the chain moves to the Deleted phase and things like consumer id to client id // associations are deleted, but the chain metadata and the chain id are not deleted func (s *SingleValidatorProviderSuite) TestProviderRemoveConsumer() { - testAcc, testAccKey, err := s.GetUnusedTestingAddresss() + testAcc, testAccKey, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) // Test removing a chain @@ -516,9 +516,9 @@ func (s *SingleValidatorProviderSuite) TestProviderRemoveConsumer() { // Confirm that only the owner can send MsgUpdateConsumer, MsgRemoveConsumer // Confirm that ownership can be transferred to a different address -> results in the "old" owner losing ownership func (s *SingleValidatorProviderSuite) TestProviderOwnerChecks() { - testAcc1, testAccKey1, err := s.GetUnusedTestingAddresss() + testAcc1, testAccKey1, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) - testAcc2, testAccKey2, err := s.GetUnusedTestingAddresss() + testAcc2, testAccKey2, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) // Create an opt-in chain chainName := "providerOwnerChecks-1" @@ -627,7 +627,7 @@ func (s *SingleValidatorProviderSuite) TestProviderOwnerChecks() { // Confirms that existing queued parameters, scheduled for update after the unbonding period, can be canceled if a new MsgUpdateConsumer // is sent with values identical to the current infraction parameters for that chain. func (s *SingleValidatorProviderSuite) TestInfractionParameters() { - testAcc, testAccKey, err := s.GetUnusedTestingAddresss() + testAcc, testAccKey, err := s.Provider.GetUnusedTestingAddresss() s.Require().NoError(err) defaultInfractionParams := defaultInfractionParams() diff --git a/tests/interchain/provider_suite.go b/tests/interchain/provider_suite.go index 280c34ba7c..0d69027973 100644 --- a/tests/interchain/provider_suite.go +++ b/tests/interchain/provider_suite.go @@ -3,9 +3,7 @@ package interchain import ( "context" "cosmos/interchain-security/tests/interchain/chainsuite" - "fmt" "strconv" - "sync" "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" "github.com/stretchr/testify/suite" @@ -16,12 +14,9 @@ type ProviderSuite struct { Provider *chainsuite.Chain ValidatorNodes int ctx context.Context - walletMtx sync.Mutex - walletsInUse map[int]bool } func (s *ProviderSuite) SetupSuite() { - s.walletsInUse = make(map[int]bool) ctx, err := chainsuite.NewSuiteContext(&s.Suite) s.Require().NoError(err) s.ctx = ctx @@ -36,21 +31,6 @@ func (s *ProviderSuite) GetContext() context.Context { return s.ctx } -// GetUnusedTestingAddresss retrieves an unused wallet address and its key name safely -func (s *ProviderSuite) GetUnusedTestingAddresss() (formattedAddress string, keyName string, err error) { - s.walletMtx.Lock() - defer s.walletMtx.Unlock() - - for i, wallet := range s.Provider.TestWallets { - if !s.walletsInUse[i] { - s.walletsInUse[i] = true - return wallet.FormattedAddress(), wallet.KeyName(), nil - } - } - - return "", "", fmt.Errorf("no unused wallets available") -} - func providerModifiedGenesis() []cosmos.GenesisKV { return []cosmos.GenesisKV{ cosmos.NewGenesisKV("app_state.staking.params.unbonding_time", chainsuite.ProviderUnbondingTime.String()), diff --git a/tests/interchain/provider_utils.go b/tests/interchain/provider_utils.go index a6d78f586f..7f21e720bc 100644 --- a/tests/interchain/provider_utils.go +++ b/tests/interchain/provider_utils.go @@ -2,9 +2,12 @@ package interchain import ( "cosmos/interchain-security/tests/interchain/chainsuite" + "fmt" + "strings" "time" "cosmossdk.io/math" + sdkmath "cosmossdk.io/math" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" providertypes "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" ) @@ -105,3 +108,12 @@ func convertJsonToInfractionParameters(jsonParams chainsuite.InfractionParams) * }, } } + +func StrToSDKInt(s string) (sdkmath.Int, error) { + s, _, _ = strings.Cut(s, ".") + i, ok := sdkmath.NewIntFromString(s) + if !ok { + return sdkmath.Int{}, fmt.Errorf("s: %s", s) + } + return i, nil +}