From 65cce0ca58c125a44fe52148d8a7c46416cd2ddd Mon Sep 17 00:00:00 2001 From: Reece Williams <31943163+Reecepbcups@users.noreply.github.com> Date: Wed, 8 May 2024 14:44:10 -0500 Subject: [PATCH] test(ics): validate consumer transactions execute (#1115) * base WIP * try the prev attempt with new migration * attempting to bring back old + migration, no work * working * reduce complexity * add p->c IBC test (fails due to TRYOPEN) * rm unused * move ICS functions to their own .go * StartRelayer right after build with longer history * touchups * add back `StopRelayer` before `StartRelayer` * refactor: `FinishICSProviderSetup` * multiple ICS version checks (v3.1, 3.3, 4.0) * local-ic: call FinishICSProviderSetup on setup * rm `interchaintest.DefaultBlockDatabaseFilepath` * rm `interchaintest.DefaultBlockDatabaseFilepath` from other test --- chain/cosmos/cosmos_chain.go | 327 -------------- chain/cosmos/ics.go | 407 ++++++++++++++++++ examples/cosmos/chain_genesis_stake_test.go | 10 +- examples/cosmos/sdk_boundary_test.go | 10 +- .../rust-optimizer/rust_optimizer_test.go | 10 +- .../workspace_optimizer_test.go | 9 +- examples/ibc/ics_test.go | 134 +++++- examples/ibc/wasm/wasm_ibc_test.go | 10 +- local-interchain/interchain/start.go | 16 + 9 files changed, 558 insertions(+), 375 deletions(-) create mode 100644 chain/cosmos/ics.go diff --git a/chain/cosmos/cosmos_chain.go b/chain/cosmos/cosmos_chain.go index 4c82ae709..04e7f6136 100644 --- a/chain/cosmos/cosmos_chain.go +++ b/chain/cosmos/cosmos_chain.go @@ -5,16 +5,13 @@ import ( "context" "crypto/sha256" "encoding/hex" - "encoding/json" "fmt" "io" "math" "os" - "path" "strconv" "strings" "sync" - "time" sdkmath "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/codec" @@ -26,7 +23,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" - govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" paramsutils "github.com/cosmos/cosmos-sdk/x/params/client/utils" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" // nolint:staticcheck chanTypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" @@ -34,7 +30,6 @@ import ( dockertypes "github.com/docker/docker/api/types" volumetypes "github.com/docker/docker/api/types/volume" "github.com/docker/docker/client" - "github.com/icza/dyno" "github.com/strangelove-ventures/interchaintest/v8/blockdb" wasmtypes "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos/08-wasm-types" "github.com/strangelove-ventures/interchaintest/v8/chain/internal/tendermint" @@ -42,14 +37,9 @@ import ( "github.com/strangelove-ventures/interchaintest/v8/ibc" "github.com/strangelove-ventures/interchaintest/v8/testutil" "go.uber.org/zap" - "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" ) -var ( - DefaultProviderUnbondingPeriod = 336 * time.Hour -) - // CosmosChain is a local docker testnet for a Cosmos SDK chain. // Implements the ibc.Chain interface. type CosmosChain struct { @@ -1044,323 +1034,6 @@ func (c *CosmosChain) Start(testName string, ctx context.Context, additionalGene return testutil.WaitForBlocks(ctx, 2, c.getFullNode()) } -// Bootstraps the provider chain and starts it from genesis -func (c *CosmosChain) StartProvider(testName string, ctx context.Context, additionalGenesisWallets ...ibc.WalletAmount) error { - existingFunc := c.cfg.ModifyGenesis - c.cfg.ModifyGenesis = func(cc ibc.ChainConfig, b []byte) ([]byte, error) { - var err error - b, err = ModifyGenesis([]GenesisKV{ - NewGenesisKV("app_state.gov.params.voting_period", "10s"), - NewGenesisKV("app_state.gov.params.max_deposit_period", "10s"), - NewGenesisKV("app_state.gov.params.min_deposit.0.denom", c.cfg.Denom), - })(cc, b) - if err != nil { - return nil, err - } - if existingFunc != nil { - return existingFunc(cc, b) - } - return b, nil - } - - const proposerKeyName = "proposer" - if err := c.CreateKey(ctx, proposerKeyName); err != nil { - return fmt.Errorf("failed to add proposer key: %s", err) - } - - proposerAddr, err := c.getFullNode().AccountKeyBech32(ctx, proposerKeyName) - if err != nil { - return fmt.Errorf("failed to get proposer key: %s", err) - } - - proposer := ibc.WalletAmount{ - Address: proposerAddr, - Denom: c.cfg.Denom, - Amount: sdkmath.NewInt(10_000_000_000_000), - } - - additionalGenesisWallets = append(additionalGenesisWallets, proposer) - - if err := c.Start(testName, ctx, additionalGenesisWallets...); err != nil { - return err - } - - trustingPeriod, err := time.ParseDuration(c.cfg.TrustingPeriod) - if err != nil { - return fmt.Errorf("failed to parse trusting period in 'StartProvider': %w", err) - } - - for _, consumer := range c.Consumers { - prop := ccvclient.ConsumerAdditionProposalJSON{ - Title: fmt.Sprintf("Addition of %s consumer chain", consumer.cfg.Name), - Summary: "Proposal to add new consumer chain", - ChainId: consumer.cfg.ChainID, - InitialHeight: clienttypes.Height{RevisionNumber: clienttypes.ParseChainID(consumer.cfg.ChainID), RevisionHeight: 1}, - GenesisHash: []byte("gen_hash"), - BinaryHash: []byte("bin_hash"), - SpawnTime: time.Now(), // Client on provider tracking consumer will be created as soon as proposal passes - - // TODO fetch or default variables - BlocksPerDistributionTransmission: 1000, - CcvTimeoutPeriod: 2419200000000000, - TransferTimeoutPeriod: 3600000000000, - ConsumerRedistributionFraction: "0.75", - HistoricalEntries: 10000, - UnbondingPeriod: trustingPeriod, - Deposit: "100000000" + c.cfg.Denom, - } - - height, err := c.Height(ctx) - if err != nil { - return fmt.Errorf("failed to query provider height before consumer addition proposal: %w", err) - } - - propTx, err := c.ConsumerAdditionProposal(ctx, proposerKeyName, prop) - if err != nil { - return err - } - - propID, err := strconv.ParseUint(propTx.ProposalID, 10, 64) - if err != nil { - return fmt.Errorf("failed to parse proposal id: %w", err) - } - - if err := c.VoteOnProposalAllValidators(ctx, propID, ProposalVoteYes); err != nil { - return err - } - - _, err = PollForProposalStatus(ctx, c, height, height+10, propID, govv1beta1.StatusPassed) - if err != nil { - return fmt.Errorf("proposal status did not change to passed in expected number of blocks: %w", err) - } - } - - return nil -} - -const ( - icsVer330 = "v3.3.0" - icsVer400 = "v4.0.0" -) - -func (c *CosmosChain) transformCCVState(ctx context.Context, ccvState []byte, consumerVersion, providerVersion string, icsCfg ibc.ICSConfig) ([]byte, error) { - // If they're both under 3.3.0, or if they're the same version, we don't need to transform the state. - if semver.MajorMinor(providerVersion) == semver.MajorMinor(consumerVersion) || - (semver.Compare(providerVersion, icsVer330) < 0 && semver.Compare(consumerVersion, icsVer330) < 0) { - return ccvState, nil - } - var imageVersion, toVersion string - // The trick here is that when we convert the state to a consumer < 3.3.0, we need a converter that knows about that version; those are >= 4.0.0, and need a --to flag. - // Other than that, this is a question of using whichever version is newer. If it's the provider's, we need a --to flag to tell it the consumer version. - // If it's the consumer's, we don't need a --to flag cause it'll assume the consumer version. - if semver.Compare(providerVersion, icsVer330) >= 0 && semver.Compare(providerVersion, consumerVersion) > 0 { - imageVersion = icsVer400 - if semver.Compare(providerVersion, icsVer400) > 0 { - imageVersion = providerVersion - } - toVersion = semver.Major(consumerVersion) - if toVersion == "v3" { - toVersion = semver.MajorMinor(consumerVersion) - } - } else { - imageVersion = consumerVersion - } - - if icsCfg.ProviderVerOverride != "" { - imageVersion = icsCfg.ProviderVerOverride - } - if icsCfg.ConsumerVerOverride != "" { - toVersion = icsCfg.ConsumerVerOverride - } - - c.log.Info("Transforming CCV state", zap.String("provider", providerVersion), zap.String("consumer", consumerVersion), zap.String("imageVersion", imageVersion), zap.String("toVersion", toVersion)) - - err := c.GetNode().WriteFile(ctx, ccvState, "ccvconsumer.json") - if err != nil { - return nil, fmt.Errorf("failed to write ccv state to file: %w", err) - } - job := dockerutil.NewImage(c.log, c.GetNode().DockerClient, c.GetNode().NetworkID, - c.GetNode().TestName, "ghcr.io/strangelove-ventures/heighliner/ics", imageVersion, - ) - cmd := []string{"interchain-security-cd", "genesis", "transform"} - if toVersion != "" { - cmd = append(cmd, "--to", toVersion+".x") - } - cmd = append(cmd, path.Join(c.GetNode().HomeDir(), "ccvconsumer.json")) - res := job.Run(ctx, cmd, dockerutil.ContainerOptions{Binds: c.GetNode().Bind()}) - if res.Err != nil { - return nil, fmt.Errorf("failed to transform ccv state: %w", res.Err) - } - return res.Stdout, nil -} - -// Bootstraps the consumer chain and starts it from genesis -func (c *CosmosChain) StartConsumer(testName string, ctx context.Context, additionalGenesisWallets ...ibc.WalletAmount) error { - chainCfg := c.Config() - - configFileOverrides := chainCfg.ConfigFileOverrides - - eg := new(errgroup.Group) - // Initialize validators and fullnodes. - for _, v := range c.Nodes() { - v := v - eg.Go(func() error { - if err := v.InitFullNodeFiles(ctx); err != nil { - return err - } - for configFile, modifiedConfig := range configFileOverrides { - modifiedToml, ok := modifiedConfig.(testutil.Toml) - if !ok { - return fmt.Errorf("provided toml override for file %s is of type (%T). Expected (DecodedToml)", configFile, modifiedConfig) - } - if err := testutil.ModifyTomlConfigFile( - ctx, - v.logger(), - v.DockerClient, - v.TestName, - v.VolumeName, - configFile, - modifiedToml, - ); err != nil { - return err - } - } - return nil - }) - } - - // wait for this to finish - if err := eg.Wait(); err != nil { - return err - } - - // Copy provider priv val keys to these nodes - for i, val := range c.Provider.Validators { - privVal, err := val.PrivValFileContent(ctx) - if err != nil { - return err - } - if err := c.Validators[i].OverwritePrivValFile(ctx, privVal); err != nil { - return err - } - } - - if c.cfg.PreGenesis != nil { - err := c.cfg.PreGenesis(chainCfg) - if err != nil { - return err - } - } - - validator0 := c.Validators[0] - - for _, wallet := range additionalGenesisWallets { - if err := validator0.AddGenesisAccount(ctx, wallet.Address, []types.Coin{{Denom: wallet.Denom, Amount: sdkmath.NewInt(wallet.Amount.Int64())}}); err != nil { - return err - } - } - - genbz, err := validator0.GenesisFileContent(ctx) - if err != nil { - return err - } - - ccvStateMarshaled, _, err := c.Provider.GetNode().ExecQuery(ctx, "provider", "consumer-genesis", c.cfg.ChainID) - if err != nil { - return fmt.Errorf("failed to query provider for ccv state: %w", err) - } - c.log.Info("BEFORE MIGRATION!", zap.String("GEN", string(ccvStateMarshaled))) - - consumerICS := c.GetNode().ICSVersion(ctx) - providerICS := c.Provider.GetNode().ICSVersion(ctx) - ccvStateMarshaled, err = c.transformCCVState(ctx, ccvStateMarshaled, consumerICS, providerICS, chainCfg.InterchainSecurityConfig) - c.log.Info("HERE STATE!", zap.String("GEN", string(ccvStateMarshaled))) - if err != nil { - return fmt.Errorf("failed to marshal ccv state to json: %w", err) - } - - var ccvStateUnmarshaled interface{} - if err := json.Unmarshal(ccvStateMarshaled, &ccvStateUnmarshaled); err != nil { - return fmt.Errorf("failed to unmarshal ccv state json: %w", err) - } - - var genesisJson interface{} - if err := json.Unmarshal(genbz, &genesisJson); err != nil { - return fmt.Errorf("failed to unmarshal genesis json: %w", err) - } - - if err := dyno.Set(genesisJson, ccvStateUnmarshaled, "app_state", "ccvconsumer"); err != nil { - return fmt.Errorf("failed to populate ccvconsumer genesis state: %w", err) - } - - if genbz, err = json.Marshal(genesisJson); err != nil { - return fmt.Errorf("failed to marshal genesis bytes to json: %w", err) - } - - genbz = bytes.ReplaceAll(genbz, []byte(`"stake"`), []byte(fmt.Sprintf(`"%s"`, chainCfg.Denom))) - - if c.cfg.ModifyGenesis != nil { - genbz, err = c.cfg.ModifyGenesis(chainCfg, genbz) - if err != nil { - return err - } - } - - // Provide EXPORT_GENESIS_FILE_PATH and EXPORT_GENESIS_CHAIN to help debug genesis file - exportGenesis := os.Getenv("EXPORT_GENESIS_FILE_PATH") - exportGenesisChain := os.Getenv("EXPORT_GENESIS_CHAIN") - if exportGenesis != "" && exportGenesisChain == c.cfg.Name { - c.log.Debug("Exporting genesis file", - zap.String("chain", exportGenesisChain), - zap.String("path", exportGenesis), - ) - _ = os.WriteFile(exportGenesis, genbz, 0600) - } - - chainNodes := c.Nodes() - - for _, cn := range chainNodes { - if err := cn.OverwriteGenesisFile(ctx, genbz); err != nil { - return err - } - } - - if err := chainNodes.LogGenesisHashes(ctx); err != nil { - return err - } - - eg, egCtx := errgroup.WithContext(ctx) - for _, n := range chainNodes { - n := n - eg.Go(func() error { - return n.CreateNodeContainer(egCtx) - }) - } - if err := eg.Wait(); err != nil { - return err - } - - peers := chainNodes.PeerString(ctx) - - eg, egCtx = errgroup.WithContext(ctx) - for _, n := range chainNodes { - n := n - c.log.Info("Starting container", zap.String("container", n.Name())) - eg.Go(func() error { - if err := n.SetPeers(egCtx, peers); err != nil { - return err - } - return n.StartContainer(egCtx) - }) - } - if err := eg.Wait(); err != nil { - return err - } - - // Wait for 5 blocks before considering the chains "started" - return testutil.WaitForBlocks(ctx, 5, c.getFullNode()) -} - // Height implements ibc.Chain func (c *CosmosChain) Height(ctx context.Context) (int64, error) { return c.getFullNode().Height(ctx) diff --git a/chain/cosmos/ics.go b/chain/cosmos/ics.go new file mode 100644 index 000000000..d81e88180 --- /dev/null +++ b/chain/cosmos/ics.go @@ -0,0 +1,407 @@ +package cosmos + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "path" + "strconv" + "time" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/types" + govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" // nolint:staticcheck + ccvclient "github.com/cosmos/interchain-security/v5/x/ccv/provider/client" + "github.com/icza/dyno" + "github.com/strangelove-ventures/interchaintest/v8/dockerutil" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/testreporter" + "github.com/strangelove-ventures/interchaintest/v8/testutil" + "go.uber.org/zap" + "golang.org/x/mod/semver" + "golang.org/x/sync/errgroup" + + stakingttypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +const ( + icsVer330 = "v3.3.0" + icsVer400 = "v4.0.0" +) + +// FinishICSProviderSetup sets up the base of an ICS connection with respect to the relayer, provider actions, and flushing of packets. +// 1. Stop the relayer, then start it back up. This completes the ICS20-1 transfer channel setup. +// - You must set look-back block history >100 blocks in [interchaintest.NewBuiltinRelayerFactory]. +// +// 2. Get the first provider validator, and delegate 1,000,000denom to it. This triggers a CometBFT power increase of 1. +// 3. Flush the pending ICS packets to the consumer chain. +func (c *CosmosChain) FinishICSProviderSetup(ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, ibcPath string) error { + // Restart the relayer to finish IBC transfer connection w/ ics20-1 link + if err := r.StopRelayer(ctx, eRep); err != nil { + return fmt.Errorf("failed to stop relayer: %w", err) + } + if err := r.StartRelayer(ctx, eRep); err != nil { + return fmt.Errorf("failed to start relayer: %w", err) + } + + // perform provider delegation to complete provider<>consumer channel connection + stakingVals, err := c.StakingQueryValidators(ctx, stakingttypes.BondStatusBonded) + if err != nil { + return fmt.Errorf("failed to query validators: %w", err) + } + + providerVal := stakingVals[0] + + beforeDel, err := c.StakingQueryDelegationsTo(ctx, providerVal.OperatorAddress) + if err != nil { + return fmt.Errorf("failed to query delegations to validator: %w", err) + } + + err = c.GetNode().StakingDelegate(ctx, "validator", providerVal.OperatorAddress, fmt.Sprintf("1000000%s", c.Config().Denom)) + if err != nil { + return fmt.Errorf("failed to delegate to validator: %w", err) + } + + afterDel, err := c.StakingQueryDelegationsTo(ctx, providerVal.OperatorAddress) + if err != nil { + return fmt.Errorf("failed to query delegations to validator: %w", err) + } + + if afterDel[0].Balance.Amount.LT(beforeDel[0].Balance.Amount) { + return fmt.Errorf("delegation failed: %w", err) + } + + return c.FlushPendingICSPackets(ctx, r, eRep, ibcPath) +} + +// FlushPendingICSPackets flushes the pending ICS packets to the consumer chain from the "provider" port. +func (c *CosmosChain) FlushPendingICSPackets(ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, ibcPath string) error { + channels, err := r.GetChannels(ctx, eRep, c.cfg.ChainID) + if err != nil { + return fmt.Errorf("failed to get channels: %w", err) + } + + ICSChannel := "" + for _, channel := range channels { + if channel.PortID == "provider" { + ICSChannel = channel.ChannelID + } + } + + return r.Flush(ctx, eRep, ibcPath, ICSChannel) +} + +// Bootstraps the provider chain and starts it from genesis +func (c *CosmosChain) StartProvider(testName string, ctx context.Context, additionalGenesisWallets ...ibc.WalletAmount) error { + existingFunc := c.cfg.ModifyGenesis + c.cfg.ModifyGenesis = func(cc ibc.ChainConfig, b []byte) ([]byte, error) { + var err error + b, err = ModifyGenesis([]GenesisKV{ + NewGenesisKV("app_state.gov.params.voting_period", "10s"), + NewGenesisKV("app_state.gov.params.max_deposit_period", "10s"), + NewGenesisKV("app_state.gov.params.min_deposit.0.denom", c.cfg.Denom), + })(cc, b) + if err != nil { + return nil, err + } + if existingFunc != nil { + return existingFunc(cc, b) + } + return b, nil + } + + const proposerKeyName = "proposer" + if err := c.CreateKey(ctx, proposerKeyName); err != nil { + return fmt.Errorf("failed to add proposer key: %s", err) + } + + proposerAddr, err := c.getFullNode().AccountKeyBech32(ctx, proposerKeyName) + if err != nil { + return fmt.Errorf("failed to get proposer key: %s", err) + } + + proposer := ibc.WalletAmount{ + Address: proposerAddr, + Denom: c.cfg.Denom, + Amount: sdkmath.NewInt(10_000_000_000_000), + } + + additionalGenesisWallets = append(additionalGenesisWallets, proposer) + + if err := c.Start(testName, ctx, additionalGenesisWallets...); err != nil { + return err + } + + trustingPeriod, err := time.ParseDuration(c.cfg.TrustingPeriod) + if err != nil { + return fmt.Errorf("failed to parse trusting period in 'StartProvider': %w", err) + } + + for _, consumer := range c.Consumers { + prop := ccvclient.ConsumerAdditionProposalJSON{ + Title: fmt.Sprintf("Addition of %s consumer chain", consumer.cfg.Name), + Summary: "Proposal to add new consumer chain", + ChainId: consumer.cfg.ChainID, + InitialHeight: clienttypes.Height{RevisionNumber: clienttypes.ParseChainID(consumer.cfg.ChainID), RevisionHeight: 1}, + GenesisHash: []byte("gen_hash"), + BinaryHash: []byte("bin_hash"), + SpawnTime: time.Now(), // Client on provider tracking consumer will be created as soon as proposal passes + + // TODO fetch or default variables + BlocksPerDistributionTransmission: 1000, + CcvTimeoutPeriod: trustingPeriod * 2, + TransferTimeoutPeriod: trustingPeriod, + ConsumerRedistributionFraction: "0.75", + HistoricalEntries: 10000, + UnbondingPeriod: trustingPeriod, + Deposit: "100000000" + c.cfg.Denom, + } + + height, err := c.Height(ctx) + if err != nil { + return fmt.Errorf("failed to query provider height before consumer addition proposal: %w", err) + } + + propTx, err := c.ConsumerAdditionProposal(ctx, proposerKeyName, prop) + if err != nil { + return err + } + + propID, err := strconv.ParseUint(propTx.ProposalID, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse proposal id: %w", err) + } + + if err := c.VoteOnProposalAllValidators(ctx, propID, ProposalVoteYes); err != nil { + return err + } + + _, err = PollForProposalStatus(ctx, c, height, height+10, propID, govv1beta1.StatusPassed) + if err != nil { + return fmt.Errorf("proposal status did not change to passed in expected number of blocks: %w", err) + } + } + + return nil +} + +// Bootstraps the consumer chain and starts it from genesis +func (c *CosmosChain) StartConsumer(testName string, ctx context.Context, additionalGenesisWallets ...ibc.WalletAmount) error { + chainCfg := c.Config() + + configFileOverrides := chainCfg.ConfigFileOverrides + + eg := new(errgroup.Group) + // Initialize validators and fullnodes. + for _, v := range c.Nodes() { + v := v + eg.Go(func() error { + if err := v.InitFullNodeFiles(ctx); err != nil { + return err + } + for configFile, modifiedConfig := range configFileOverrides { + modifiedToml, ok := modifiedConfig.(testutil.Toml) + if !ok { + return fmt.Errorf("provided toml override for file %s is of type (%T). Expected (DecodedToml)", configFile, modifiedConfig) + } + if err := testutil.ModifyTomlConfigFile( + ctx, + v.logger(), + v.DockerClient, + v.TestName, + v.VolumeName, + configFile, + modifiedToml, + ); err != nil { + return err + } + } + return nil + }) + } + + // wait for this to finish + if err := eg.Wait(); err != nil { + return err + } + + // Copy provider priv val keys to these nodes + for i, val := range c.Provider.Validators { + privVal, err := val.PrivValFileContent(ctx) + if err != nil { + return err + } + if err := c.Validators[i].OverwritePrivValFile(ctx, privVal); err != nil { + return err + } + } + + if c.cfg.PreGenesis != nil { + err := c.cfg.PreGenesis(chainCfg) + if err != nil { + return err + } + } + + validator0 := c.Validators[0] + + for _, wallet := range additionalGenesisWallets { + if err := validator0.AddGenesisAccount(ctx, wallet.Address, []types.Coin{{Denom: wallet.Denom, Amount: sdkmath.NewInt(wallet.Amount.Int64())}}); err != nil { + return err + } + } + + genbz, err := validator0.GenesisFileContent(ctx) + if err != nil { + return err + } + + ccvStateMarshaled, _, err := c.Provider.GetNode().ExecQuery(ctx, "provider", "consumer-genesis", c.cfg.ChainID) + if err != nil { + return fmt.Errorf("failed to query provider for ccv state: %w", err) + } + + consumerICS := c.GetNode().ICSVersion(ctx) + providerICS := c.Provider.GetNode().ICSVersion(ctx) + ccvStateMarshaled, err = c.transformCCVState(ctx, ccvStateMarshaled, consumerICS, providerICS, chainCfg.InterchainSecurityConfig) + if err != nil { + return fmt.Errorf("failed to transform ccv state: %w", err) + } + + c.log.Info("HERE STATE!", zap.String("GEN", string(ccvStateMarshaled))) + + var ccvStateUnmarshaled interface{} + if err := json.Unmarshal(ccvStateMarshaled, &ccvStateUnmarshaled); err != nil { + return fmt.Errorf("failed to unmarshal ccv state json: %w", err) + } + + var genesisJson interface{} + if err := json.Unmarshal(genbz, &genesisJson); err != nil { + return fmt.Errorf("failed to unmarshal genesis json: %w", err) + } + + if err := dyno.Set(genesisJson, ccvStateUnmarshaled, "app_state", "ccvconsumer"); err != nil { + return fmt.Errorf("failed to populate ccvconsumer genesis state: %w", err) + } + + if genbz, err = json.Marshal(genesisJson); err != nil { + return fmt.Errorf("failed to marshal genesis bytes to json: %w", err) + } + + genbz = bytes.ReplaceAll(genbz, []byte(`"stake"`), []byte(fmt.Sprintf(`"%s"`, chainCfg.Denom))) + + if c.cfg.ModifyGenesis != nil { + genbz, err = c.cfg.ModifyGenesis(chainCfg, genbz) + if err != nil { + return err + } + } + + // Provide EXPORT_GENESIS_FILE_PATH and EXPORT_GENESIS_CHAIN to help debug genesis file + exportGenesis := os.Getenv("EXPORT_GENESIS_FILE_PATH") + exportGenesisChain := os.Getenv("EXPORT_GENESIS_CHAIN") + if exportGenesis != "" && exportGenesisChain == c.cfg.Name { + c.log.Debug("Exporting genesis file", + zap.String("chain", exportGenesisChain), + zap.String("path", exportGenesis), + ) + _ = os.WriteFile(exportGenesis, genbz, 0600) + } + + chainNodes := c.Nodes() + + for _, cn := range chainNodes { + if err := cn.OverwriteGenesisFile(ctx, genbz); err != nil { + return err + } + } + + if err := chainNodes.LogGenesisHashes(ctx); err != nil { + return err + } + + eg, egCtx := errgroup.WithContext(ctx) + for _, n := range chainNodes { + n := n + eg.Go(func() error { + return n.CreateNodeContainer(egCtx) + }) + } + if err := eg.Wait(); err != nil { + return err + } + + peers := chainNodes.PeerString(ctx) + + eg, egCtx = errgroup.WithContext(ctx) + for _, n := range chainNodes { + n := n + c.log.Info("Starting container", zap.String("container", n.Name())) + eg.Go(func() error { + if err := n.SetPeers(egCtx, peers); err != nil { + return err + } + return n.StartContainer(egCtx) + }) + } + if err := eg.Wait(); err != nil { + return err + } + + // Wait for 5 blocks before considering the chains "started" + return testutil.WaitForBlocks(ctx, 5, c.getFullNode()) +} + +func (c *CosmosChain) transformCCVState(ctx context.Context, ccvState []byte, consumerVersion, providerVersion string, icsCfg ibc.ICSConfig) ([]byte, error) { + // If they're both under 3.3.0, or if they're the same version, we don't need to transform the state. + if semver.MajorMinor(providerVersion) == semver.MajorMinor(consumerVersion) || + (semver.Compare(providerVersion, icsVer330) < 0 && semver.Compare(consumerVersion, icsVer330) < 0) { + return ccvState, nil + } + var imageVersion, toVersion string + // The trick here is that when we convert the state to a consumer < 3.3.0, we need a converter that knows about that version; those are >= 4.0.0, and need a --to flag. + // Other than that, this is a question of using whichever version is newer. If it's the provider's, we need a --to flag to tell it the consumer version. + // If it's the consumer's, we don't need a --to flag cause it'll assume the consumer version. + if semver.Compare(providerVersion, icsVer330) >= 0 && semver.Compare(providerVersion, consumerVersion) > 0 { + imageVersion = icsVer400 + if semver.Compare(providerVersion, icsVer400) > 0 { + imageVersion = providerVersion + } + toVersion = semver.Major(consumerVersion) + if toVersion == "v3" { + toVersion = semver.MajorMinor(consumerVersion) + } + } else { + imageVersion = consumerVersion + } + + if icsCfg.ProviderVerOverride != "" { + imageVersion = icsCfg.ProviderVerOverride + } + if icsCfg.ConsumerVerOverride != "" { + toVersion = icsCfg.ConsumerVerOverride + } + + c.log.Info("Transforming CCV state", zap.String("provider", providerVersion), zap.String("consumer", consumerVersion), zap.String("imageVersion", imageVersion), zap.String("toVersion", toVersion)) + + err := c.GetNode().WriteFile(ctx, ccvState, "ccvconsumer.json") + if err != nil { + return nil, fmt.Errorf("failed to write ccv state to file: %w", err) + } + job := dockerutil.NewImage(c.log, c.GetNode().DockerClient, c.GetNode().NetworkID, + c.GetNode().TestName, "ghcr.io/strangelove-ventures/heighliner/ics", imageVersion, + ) + cmd := []string{"interchain-security-cd", "genesis", "transform"} + if toVersion != "" { + cmd = append(cmd, "--to", toVersion+".x") + } + cmd = append(cmd, path.Join(c.GetNode().HomeDir(), "ccvconsumer.json")) + res := job.Run(ctx, cmd, dockerutil.ContainerOptions{Binds: c.GetNode().Bind()}) + if res.Err != nil { + return nil, fmt.Errorf("failed to transform ccv state: %w", res.Err) + } + return res.Stdout, nil +} diff --git a/examples/cosmos/chain_genesis_stake_test.go b/examples/cosmos/chain_genesis_stake_test.go index e59dbdf26..138cf43c4 100644 --- a/examples/cosmos/chain_genesis_stake_test.go +++ b/examples/cosmos/chain_genesis_stake_test.go @@ -58,11 +58,11 @@ func TestChainGenesisUnequalStake(t *testing.T) { rep := testreporter.NewNopReporter() err = ic.Build(context.Background(), rep.RelayerExecReporter(t), interchaintest.InterchainBuildOptions{ - TestName: t.Name(), - Client: client, - NetworkID: network, - BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), - SkipPathCreation: false, + TestName: t.Name(), + Client: client, + NetworkID: network, + // BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), + SkipPathCreation: false, }) require.NoError(t, err) t.Cleanup(func() { diff --git a/examples/cosmos/sdk_boundary_test.go b/examples/cosmos/sdk_boundary_test.go index b56165749..f7ff3e568 100644 --- a/examples/cosmos/sdk_boundary_test.go +++ b/examples/cosmos/sdk_boundary_test.go @@ -101,11 +101,11 @@ func TestSDKBoundaries(t *testing.T) { rep := testreporter.NewNopReporter() require.NoError(t, ic.Build(ctx, rep.RelayerExecReporter(t), interchaintest.InterchainBuildOptions{ - TestName: t.Name(), - Client: client, - NetworkID: network, - BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), - SkipPathCreation: false, + TestName: t.Name(), + Client: client, + NetworkID: network, + // BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), + SkipPathCreation: false, })) t.Cleanup(func() { _ = ic.Close() diff --git a/examples/cosmwasm/rust-optimizer/rust_optimizer_test.go b/examples/cosmwasm/rust-optimizer/rust_optimizer_test.go index e1e3110b8..7f8aa8fca 100644 --- a/examples/cosmwasm/rust-optimizer/rust_optimizer_test.go +++ b/examples/cosmwasm/rust-optimizer/rust_optimizer_test.go @@ -60,11 +60,11 @@ func TestRustOptimizerContract(t *testing.T) { // Build interchain require.NoError(t, ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ - TestName: t.Name(), - Client: client, - NetworkID: network, - BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), - SkipPathCreation: true, + TestName: t.Name(), + Client: client, + NetworkID: network, + // BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), + SkipPathCreation: true, })) t.Cleanup(func() { _ = ic.Close() diff --git a/examples/cosmwasm/workspace-optimizer/workspace_optimizer_test.go b/examples/cosmwasm/workspace-optimizer/workspace_optimizer_test.go index 94a021c5e..1c630c71e 100644 --- a/examples/cosmwasm/workspace-optimizer/workspace_optimizer_test.go +++ b/examples/cosmwasm/workspace-optimizer/workspace_optimizer_test.go @@ -60,11 +60,10 @@ func TestWorkspaceOptimizerContracts(t *testing.T) { // Build interchain require.NoError(t, ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ - TestName: t.Name(), - Client: client, - NetworkID: network, - BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), - SkipPathCreation: true, + TestName: t.Name(), + Client: client, + NetworkID: network, + SkipPathCreation: true, })) t.Cleanup(func() { _ = ic.Close() diff --git a/examples/ibc/ics_test.go b/examples/ibc/ics_test.go index f7cc04c5c..a9b568a4b 100644 --- a/examples/ibc/ics_test.go +++ b/examples/ibc/ics_test.go @@ -3,52 +3,80 @@ package ibc_test import ( "context" "fmt" + "strings" "testing" - "time" + "cosmossdk.io/math" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + ibcconntypes "github.com/cosmos/ibc-go/v8/modules/core/03-connection/types" interchaintest "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/relayer" "github.com/strangelove-ventures/interchaintest/v8/testreporter" - "github.com/strangelove-ventures/interchaintest/v8/testutil" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) +var ( + icsVersions = []string{"v3.1.0", "v3.3.0", "v4.0.0"} + vals = 1 + fNodes = 0 + providerChainID = "provider-1" +) + // This tests Cosmos Interchain Security, spinning up a provider and a single consumer chain. +// go test -timeout 3000s -run ^TestICS$ github.com/strangelove-ventures/interchaintest/v8/examples/ibc -v -test.short func TestICS(t *testing.T) { if testing.Short() { - t.Skip("skipping in short mode") + ver := icsVersions[0] + t.Logf("Running in short mode, only testing the latest ICS version: %s", ver) + icsVersions = []string{ver} + } + + for _, version := range icsVersions { + version := version + testName := "ics_" + strings.ReplaceAll(version, ".", "_") + + t.Run(testName, func(t *testing.T) { + t.Parallel() + icsTest(t, version) + }) } - t.Parallel() +} +func icsTest(t *testing.T, version string) { ctx := context.Background() - vals := 1 - fNodes := 0 + consumerBechPrefix := "cosmos" + if version == "v4.0.0" { + consumerBechPrefix = "consumer" + } - // Chain Factory cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{ { - Name: "ics-provider", Version: "v3.1.0", + Name: "ics-provider", Version: version, NumValidators: &vals, NumFullNodes: &fNodes, - ChainConfig: ibc.ChainConfig{GasAdjustment: 1.5}}, + ChainConfig: ibc.ChainConfig{GasAdjustment: 1.5, ChainID: providerChainID, TrustingPeriod: "336h"}, + }, { - Name: "ics-consumer", Version: "v3.1.0", + Name: "ics-consumer", Version: version, NumValidators: &vals, NumFullNodes: &fNodes, + ChainConfig: ibc.ChainConfig{GasAdjustment: 1.5, ChainID: "consumer-1", Bech32Prefix: consumerBechPrefix}, }, }) chains, err := cf.Chains(t.Name()) require.NoError(t, err) - provider, consumer := chains[0], chains[1] + provider, consumer := chains[0].(*cosmos.CosmosChain), chains[1].(*cosmos.CosmosChain) // Relayer Factory client, network := interchaintest.DockerSetup(t) - r := interchaintest.NewBuiltinRelayerFactory( ibc.CosmosRly, zaptest.NewLogger(t), + relayer.StartupFlags("--block-history", "100"), ).Build(t, client, network) // Prep Interchain @@ -64,24 +92,84 @@ func TestICS(t *testing.T) { Path: ibcPath, }) - // Log location - f, err := interchaintest.CreateLogFile(fmt.Sprintf("%d.json", time.Now().Unix())) - require.NoError(t, err) // Reporter/logs - rep := testreporter.NewReporter(f) + rep := testreporter.NewNopReporter() eRep := rep.RelayerExecReporter(t) // Build interchain err = ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ - TestName: t.Name(), - Client: client, - NetworkID: network, - BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), - + TestName: t.Name(), + Client: client, + NetworkID: network, SkipPathCreation: false, }) require.NoError(t, err, "failed to build interchain") - err = testutil.WaitForBlocks(ctx, 10, provider, consumer) - require.NoError(t, err, "failed to wait for blocks") + // ------------------ ICS Setup ------------------ + + // Finish the ICS provider chain initialization. + // - Restarts the relayer to connect ics20-1 transfer channel + // - Delegates tokens to the provider to update consensus value + // - Flushes the IBC state to the consumer + err = provider.FinishICSProviderSetup(ctx, r, eRep, ibcPath) + require.NoError(t, err) + + // ------------------ Test Begins ------------------ + + // Fund users + // NOTE: this has to be done after the provider delegation & IBC update to the consumer. + amt := math.NewInt(10_000_000) + users := interchaintest.GetAndFundTestUsers(t, ctx, "default", amt, consumer, provider) + consumerUser, providerUser := users[0], users[1] + + t.Run("validate consumer action executed", func(t *testing.T) { + bal, err := consumer.BankQueryBalance(ctx, consumerUser.FormattedAddress(), consumer.Config().Denom) + require.NoError(t, err) + require.EqualValues(t, amt, bal) + }) + + t.Run("provider -> consumer IBC transfer", func(t *testing.T) { + providerChannelInfo, err := r.GetChannels(ctx, eRep, provider.Config().ChainID) + require.NoError(t, err) + + channelID, err := getTransferChannel(providerChannelInfo) + require.NoError(t, err, providerChannelInfo) + + consumerChannelInfo, err := r.GetChannels(ctx, eRep, consumer.Config().ChainID) + require.NoError(t, err) + + consumerChannelID, err := getTransferChannel(consumerChannelInfo) + require.NoError(t, err, consumerChannelInfo) + + dstAddress := consumerUser.FormattedAddress() + sendAmt := math.NewInt(7) + transfer := ibc.WalletAmount{ + Address: dstAddress, + Denom: provider.Config().Denom, + Amount: sendAmt, + } + + tx, err := provider.SendIBCTransfer(ctx, channelID, providerUser.KeyName(), transfer, ibc.TransferOptions{}) + require.NoError(t, err) + require.NoError(t, tx.Validate()) + + require.NoError(t, r.Flush(ctx, eRep, ibcPath, channelID)) + + srcDenomTrace := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom("transfer", consumerChannelID, provider.Config().Denom)) + dstIbcDenom := srcDenomTrace.IBCDenom() + + consumerBal, err := consumer.BankQueryBalance(ctx, consumerUser.FormattedAddress(), dstIbcDenom) + require.NoError(t, err) + require.EqualValues(t, sendAmt, consumerBal) + }) +} + +func getTransferChannel(channels []ibc.ChannelOutput) (string, error) { + for _, channel := range channels { + if channel.PortID == "transfer" && channel.State == ibcconntypes.OPEN.String() { + return channel.ChannelID, nil + } + } + + return "", fmt.Errorf("no open transfer channel found") } diff --git a/examples/ibc/wasm/wasm_ibc_test.go b/examples/ibc/wasm/wasm_ibc_test.go index f8b344a44..ffea517dc 100644 --- a/examples/ibc/wasm/wasm_ibc_test.go +++ b/examples/ibc/wasm/wasm_ibc_test.go @@ -67,11 +67,11 @@ func TestWasmIbc(t *testing.T) { // Build interchain require.NoError(t, ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ - TestName: t.Name(), - Client: client, - NetworkID: network, - BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), - SkipPathCreation: false, + TestName: t.Name(), + Client: client, + NetworkID: network, + // BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), + SkipPathCreation: false, })) t.Cleanup(func() { _ = ic.Close() diff --git a/local-interchain/interchain/start.go b/local-interchain/interchain/start.go index 5ddc106f7..ca8acbcd6 100644 --- a/local-interchain/interchain/start.go +++ b/local-interchain/interchain/start.go @@ -132,6 +132,7 @@ func StartChain(installDir, chainCfgFile string, ac *types.AppStartConfig) { } // Add Interchain Security chain pairs together + icsProviderPaths := make(map[string]ibc.Chain) if len(icsPair) > 0 { for provider, consumers := range icsPair { var p, c ibc.Chain @@ -152,6 +153,8 @@ func StartChain(installDir, chainCfgFile string, ac *types.AppStartConfig) { logger.Info("Adding ICS pair", zap.String("provider", p.Config().ChainID), zap.String("consumer", c.Config().ChainID), zap.String("path", pathName)) + icsProviderPaths[pathName] = p + ic = ic.AddProviderConsumerLink(interchaintest.ProviderConsumerLink{ Provider: p, Consumer: c, @@ -197,6 +200,19 @@ func StartChain(installDir, chainCfgFile string, ac *types.AppStartConfig) { } } + // ICS provider setup + if len(icsProviderPaths) > 0 { + logger.Info("ICS provider setup", zap.Any("icsProviderPaths", icsProviderPaths)) + + for ibcPath, chain := range icsProviderPaths { + if provider, ok := chain.(*cosmos.CosmosChain); ok { + if err := provider.FinishICSProviderSetup(ctx, relayer, eRep, ibcPath); err != nil { + log.Fatal("FinishICSProviderSetup", err) + } + } + } + } + // Starts a non blocking REST server to take action on the chain. go func() { cosmosChains := map[string]*cosmos.CosmosChain{}