From aad58a5aef33569d2afe9276b7b5aa313b8c83cd Mon Sep 17 00:00:00 2001 From: violet <158512193+fastfadingviolets@users.noreply.github.com> Date: Thu, 24 Oct 2024 02:42:59 -0400 Subject: [PATCH] ci: Add interchaintests for user stories & v21 (#3400) * ci: Add a test for LSM happy path ops * ci: Add a test for LSM via ICA * ci: Add a test for the ICA controller module * ci: Add a PFM test * ci: Test that rewards work with sovereign -> consumer changeovers --- .github/workflows/interchain-test.yml | 2 +- tests/interchain/chainsuite/chain.go | 150 ++++++- tests/interchain/chainsuite/chain_ics.go | 145 ++----- tests/interchain/chainsuite/config.go | 103 +++-- tests/interchain/chainsuite/relayer.go | 158 ++++++-- tests/interchain/consensus_test.go | 34 +- tests/interchain/consumer_launch_test.go | 87 ++-- tests/interchain/feemarket_test.go | 221 ++++++++++ tests/interchain/go.mod | 4 +- tests/interchain/go.sum | 4 +- tests/interchain/ica_controller_test.go | 148 +++++++ tests/interchain/lsm_test.go | 493 +++++++++++++++++++++++ tests/interchain/matrix_tool/main.go | 4 + tests/interchain/permissionless_test.go | 222 +++++++++- tests/interchain/pfm_test.go | 133 ++++++ tests/interchain/unbonding_test.go | 23 +- 16 files changed, 1703 insertions(+), 228 deletions(-) create mode 100644 tests/interchain/feemarket_test.go create mode 100644 tests/interchain/ica_controller_test.go create mode 100644 tests/interchain/lsm_test.go create mode 100644 tests/interchain/pfm_test.go diff --git a/.github/workflows/interchain-test.yml b/.github/workflows/interchain-test.yml index 8de73e3264e..38b16906b1c 100644 --- a/.github/workflows/interchain-test.yml +++ b/.github/workflows/interchain-test.yml @@ -42,7 +42,7 @@ jobs: matrix: ${{fromJson(needs.prepare-matrix.outputs.matrix)}} fail-fast: false - max-parallel: 10 + max-parallel: 3 steps: - name: Check out repository code uses: actions/checkout@v4 diff --git a/tests/interchain/chainsuite/chain.go b/tests/interchain/chainsuite/chain.go index 22741701e09..cd1e6b5808f 100644 --- a/tests/interchain/chainsuite/chain.go +++ b/tests/interchain/chainsuite/chain.go @@ -237,10 +237,10 @@ func (c *Chain) GetValidatorHex(ctx context.Context, val int) (string, error) { } func getValidatorWallets(ctx context.Context, chain *Chain) ([]ValidatorWallet, error) { - wallets := make([]ValidatorWallet, ValidatorCount) + wallets := make([]ValidatorWallet, len(chain.Validators)) lock := new(sync.Mutex) eg := new(errgroup.Group) - for i := 0; i < ValidatorCount; i++ { + for i := range chain.Validators { i := i eg.Go(func() error { // This moniker is hardcoded into the chain's genesis process. @@ -309,3 +309,149 @@ func (c *Chain) GetProposalID(ctx context.Context, txhash string) (string, error } return "", fmt.Errorf("proposal ID not found in tx %s", txhash) } + +func (c *Chain) hasOrderingFlag(ctx context.Context) (bool, error) { + cmd := c.GetNode().BinCommand("tx", "interchain-accounts", "controller", "register", "--help") + stdout, _, err := c.GetNode().Exec(ctx, cmd, nil) + if err != nil { + return false, err + } + return strings.Contains(string(stdout), "ordering"), nil +} + +func (c *Chain) GetICAAddress(ctx context.Context, srcAddress string, srcConnection string) string { + var icaAddress string + + // it takes a moment for it to be created + timeoutCtx, timeoutCancel := context.WithTimeout(ctx, 90*time.Second) + defer timeoutCancel() + for timeoutCtx.Err() == nil { + time.Sleep(5 * time.Second) + stdout, _, err := c.GetNode().ExecQuery(timeoutCtx, + "interchain-accounts", "controller", "interchain-account", + srcAddress, srcConnection, + ) + if err != nil { + GetLogger(ctx).Sugar().Warnf("error querying interchain account: %s", err) + continue + } + result := map[string]interface{}{} + err = json.Unmarshal(stdout, &result) + if err != nil { + GetLogger(ctx).Sugar().Warnf("error unmarshalling interchain account: %s", err) + continue + } + icaAddress = result["address"].(string) + if icaAddress != "" { + break + } + } + return icaAddress +} + +func (c *Chain) SetupICAAccount(ctx context.Context, host *Chain, relayer *Relayer, srcAddress string, valIdx int, initialFunds int64) (string, error) { + srcChannel, err := relayer.GetTransferChannel(ctx, c, host) + if err != nil { + return "", err + } + srcConnection := srcChannel.ConnectionHops[0] + + hasOrdering, err := c.hasOrderingFlag(ctx) + if err != nil { + return "", err + } + + if hasOrdering { + _, err = c.Validators[valIdx].ExecTx(ctx, srcAddress, + "interchain-accounts", "controller", "register", + "--ordering", "ORDER_ORDERED", "--version", "", + srcConnection, + ) + } else { + _, err = c.Validators[valIdx].ExecTx(ctx, srcAddress, + "interchain-accounts", "controller", "register", + srcConnection, + ) + } + if err != nil { + return "", err + } + + icaAddress := c.GetICAAddress(ctx, srcAddress, srcConnection) + if icaAddress == "" { + return "", fmt.Errorf("ICA address not found") + } + + err = host.SendFunds(ctx, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Denom: host.Config().Denom, + Amount: sdkmath.NewInt(initialFunds), + Address: icaAddress, + }) + if err != nil { + return "", err + } + + return icaAddress, nil +} + +func (c *Chain) AddLinkedChain(ctx context.Context, testName interchaintest.TestName, relayer *Relayer, spec *interchaintest.ChainSpec) (*Chain, error) { + dockerClient, dockerNetwork := GetDockerContext(ctx) + + cf := interchaintest.NewBuiltinChainFactory( + GetLogger(ctx), + []*interchaintest.ChainSpec{spec}, + ) + + chains, err := cf.Chains(testName.Name()) + if err != nil { + return nil, err + } + cosmosChainB := chains[0].(*cosmos.CosmosChain) + relayerWallet, err := cosmosChainB.BuildRelayerWallet(ctx, "relayer-"+cosmosChainB.Config().ChainID) + if err != nil { + return nil, err + } + + ic := interchaintest.NewInterchain().AddChain(cosmosChainB, ibc.WalletAmount{ + Address: relayerWallet.FormattedAddress(), + Denom: cosmosChainB.Config().Denom, + Amount: sdkmath.NewInt(ValidatorFunds), + }) + + if err := ic.Build(ctx, GetRelayerExecReporter(ctx), interchaintest.InterchainBuildOptions{ + Client: dockerClient, + NetworkID: dockerNetwork, + TestName: testName.Name(), + }); err != nil { + return nil, err + } + + chainB, err := chainFromCosmosChain(cosmosChainB, relayerWallet) + if err != nil { + return nil, err + } + rep := GetRelayerExecReporter(ctx) + if err := relayer.SetupChainKeys(ctx, chainB); err != nil { + return nil, err + } + if err := relayer.StopRelayer(ctx, rep); err != nil { + return nil, err + } + if err := relayer.StartRelayer(ctx, rep); err != nil { + return nil, err + } + + if err := relayer.GeneratePath(ctx, rep, c.Config().ChainID, chainB.Config().ChainID, relayerTransferPathFor(c, chainB)); err != nil { + return nil, err + } + + if err := relayer.LinkPath(ctx, rep, relayerTransferPathFor(c, chainB), ibc.CreateChannelOptions{ + DestPortName: TransferPortID, + SourcePortName: TransferPortID, + Order: ibc.Unordered, + }, ibc.DefaultClientOpts()); err != nil { + return nil, err + } + + return chainB, nil +} diff --git a/tests/interchain/chainsuite/chain_ics.go b/tests/interchain/chainsuite/chain_ics.go index 0124af7773b..03f0652ffa0 100644 --- a/tests/interchain/chainsuite/chain_ics.go +++ b/tests/interchain/chainsuite/chain_ics.go @@ -15,7 +15,6 @@ import ( "github.com/tidwall/gjson" "github.com/tidwall/sjson" "go.uber.org/multierr" - "golang.org/x/mod/semver" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" ccvclient "github.com/cosmos/interchain-security/v5/x/ccv/provider/client" @@ -29,18 +28,20 @@ import ( type ConsumerBootstrapCb func(ctx context.Context, consumer *cosmos.CosmosChain) type ConsumerConfig struct { - ChainName string - Version string - Denom string - ShouldCopyProviderKey [ValidatorCount]bool - TopN int - ValidatorSetCap int - ValidatorPowerCap int - AllowInactiveVals bool - MinStake uint64 - Allowlist []string - Denylist []string - spec *interchaintest.ChainSpec + ChainName string + Version string + Denom string + ShouldCopyProviderKey [ValidatorCount]bool + TopN int + ValidatorSetCap int + ValidatorPowerCap int + AllowInactiveVals bool + MinStake uint64 + Allowlist []string + Denylist []string + InitialHeight uint64 + DistributionTransmissionChannel string + Spec *interchaintest.ChainSpec DuringDepositPeriod ConsumerBootstrapCb DuringVotingPeriod ConsumerBootstrapCb @@ -104,7 +105,8 @@ func (p *Chain) AddConsumerChain(ctx context.Context, relayer *Relayer, config C } spawnTime := time.Now().Add(ChainSpawnWait) - chainID := fmt.Sprintf("%s-%d", config.ChainName, len(p.Consumers)+1) + // We need -test- in there because certain consumer IDs are hardcoded into the binary and we can't re-launch them + chainID := fmt.Sprintf("%s-test-%d", config.ChainName, len(p.Consumers)+1) var proposalWaiter *proposalWaiter var errCh chan error @@ -123,15 +125,16 @@ func (p *Chain) AddConsumerChain(ctx context.Context, relayer *Relayer, config C } } - if config.spec == nil { - config.spec = p.DefaultConsumerChainSpec(ctx, chainID, config, spawnTime, proposalWaiter) - } - if semver.Compare(p.GetNode().ICSVersion(ctx), "v4.1.0") > 0 && config.spec.InterchainSecurityConfig.ProviderVerOverride == "" { - config.spec.InterchainSecurityConfig.ProviderVerOverride = "v4.1.0" + defaultSpec := p.DefaultConsumerChainSpec(ctx, chainID, config, spawnTime, proposalWaiter) + config.Spec = MergeChainSpecs(defaultSpec, config.Spec) + providerICS := p.GetNode().ICSVersion(ctx) + if config.Spec.InterchainSecurityConfig.ConsumerVerOverride == "" { + // This will disable the genesis transform + config.Spec.InterchainSecurityConfig.ConsumerVerOverride = providerICS } cf := interchaintest.NewBuiltinChainFactory( GetLogger(ctx), - []*interchaintest.ChainSpec{config.spec}, + []*interchaintest.ChainSpec{config.Spec}, ) chains, err := cf.Chains(p.GetNode().TestName) if err != nil { @@ -202,7 +205,7 @@ func (p *Chain) AddConsumerChain(ctx context.Context, relayer *Relayer, config C if err := relayer.StartRelayer(ctx, rep); err != nil { return nil, err } - err = connectProviderConsumer(ctx, p, consumer, relayer) + err = relayer.ConnectProviderConsumer(ctx, p, consumer) if err != nil { return nil, err } @@ -211,10 +214,14 @@ func (p *Chain) AddConsumerChain(ctx context.Context, relayer *Relayer, config C } func (p *Chain) CreateConsumerPermissionless(ctx context.Context, chainID string, config ConsumerConfig, spawnTime time.Time) error { + revisionHeight := config.InitialHeight + if revisionHeight == 0 { + revisionHeight = 1 + } initParams := &providertypes.ConsumerInitializationParameters{ - InitialHeight: clienttypes.Height{RevisionNumber: clienttypes.ParseChainID(chainID), RevisionHeight: 1}, + InitialHeight: clienttypes.Height{RevisionNumber: clienttypes.ParseChainID(chainID), RevisionHeight: revisionHeight}, SpawnTime: spawnTime, - BlocksPerDistributionTransmission: 1000, + BlocksPerDistributionTransmission: BlocksPerDistribution, CcvTimeoutPeriod: 2419200000000000, TransferTimeoutPeriod: 3600000000000, ConsumerRedistributionFraction: "0.75", @@ -222,6 +229,7 @@ func (p *Chain) CreateConsumerPermissionless(ctx context.Context, chainID string UnbondingPeriod: 1728000000000000, GenesisHash: []byte("Z2VuX2hhc2g="), BinaryHash: []byte("YmluX2hhc2g="), + DistributionTransmissionChannel: config.DistributionTransmissionChannel, } powerShapingParams := &providertypes.PowerShapingParameters{ Top_N: 0, @@ -402,6 +410,7 @@ func (p *Chain) DefaultConsumerChainSpec(ctx context.Context, chainID string, co Denom: denom, GasPrices: "0.005" + denom, GasAdjustment: 2.0, + Gas: "auto", ChainID: chainID, ConfigFileOverrides: map[string]any{ "config/config.toml": DefaultConfigToml(), @@ -451,83 +460,6 @@ func (p *Chain) DefaultConsumerChainSpec(ctx context.Context, chainID string, co } } -func connectProviderConsumer(ctx context.Context, provider *Chain, consumer *Chain, relayer *Relayer) error { - icsPath := relayerICSPathFor(provider, consumer) - rep := GetRelayerExecReporter(ctx) - if err := relayer.GeneratePath(ctx, rep, consumer.Config().ChainID, provider.Config().ChainID, icsPath); err != nil { - return err - } - - consumerClients, err := relayer.GetClients(ctx, rep, consumer.Config().ChainID) - if err != nil { - return err - } - - var consumerClient *ibc.ClientOutput - for _, client := range consumerClients { - if client.ClientState.ChainID == provider.Config().ChainID { - consumerClient = client - break - } - } - if consumerClient == nil { - return fmt.Errorf("consumer chain %s does not have a client tracking the provider chain %s", consumer.Config().ChainID, provider.Config().ChainID) - } - consumerClientID := consumerClient.ClientID - - providerClients, err := relayer.GetClients(ctx, rep, provider.Config().ChainID) - if err != nil { - return err - } - - var providerClient *ibc.ClientOutput - for _, client := range providerClients { - if client.ClientState.ChainID == consumer.Config().ChainID { - providerClient = client - break - } - } - if providerClient == nil { - return fmt.Errorf("provider chain %s does not have a client tracking the consumer chain %s for path %s on relayer %s", - provider.Config().ChainID, consumer.Config().ChainID, icsPath, relayer) - } - providerClientID := providerClient.ClientID - - if err := relayer.UpdatePath(ctx, rep, icsPath, ibc.PathUpdateOptions{ - SrcClientID: &consumerClientID, - DstClientID: &providerClientID, - }); err != nil { - return err - } - - if err := relayer.CreateConnections(ctx, rep, icsPath); err != nil { - return err - } - - if err := relayer.CreateChannel(ctx, rep, icsPath, ibc.CreateChannelOptions{ - SourcePortName: "consumer", - DestPortName: "provider", - Order: ibc.Ordered, - Version: "1", - }); err != nil { - return err - } - - tCtx, tCancel := context.WithTimeout(ctx, 30*CommitTimeout) - defer tCancel() - for tCtx.Err() == nil { - var ch *ibc.ChannelOutput - ch, err = relayer.GetTransferChannel(ctx, provider, consumer) - if err == nil && ch != nil { - break - } else if err == nil { - err = fmt.Errorf("channel not found") - } - time.Sleep(CommitTimeout) - } - return err -} - func (p *Chain) SubmitConsumerAdditionProposal(ctx context.Context, chainID string, config ConsumerConfig, spawnTime time.Time) (*proposalWaiter, chan error, error) { propWaiter := newProposalWaiter() prop := p.buildConsumerAdditionJSON(chainID, config, spawnTime) @@ -575,7 +507,7 @@ func (p *Chain) buildConsumerAdditionJSON(chainID string, config ConsumerConfig, BinaryHash: []byte("bin_hash"), SpawnTime: spawnTime, - BlocksPerDistributionTransmission: 1000, + BlocksPerDistributionTransmission: BlocksPerDistribution, CcvTimeoutPeriod: 2419200000000000, TransferTimeoutPeriod: 3600000000000, ConsumerRedistributionFraction: "0.75", @@ -644,6 +576,13 @@ func (p *Chain) CheckCCV(ctx context.Context, consumer *Chain, relayer *Relayer, } } + if err := relayer.ClearCCVChannel(ctx, p, consumer); err != nil { + return err + } + if err := testutil.WaitForBlocks(ctx, 2, p, consumer); err != nil { + return err + } + tCtx, tCancel := context.WithTimeout(ctx, 15*time.Minute) defer tCancel() var retErr error @@ -670,10 +609,6 @@ func (p *Chain) CheckCCV(ctx context.Context, consumer *Chain, relayer *Relayer, return retErr } -func relayerICSPathFor(chainA, chainB *Chain) string { - return fmt.Sprintf("ics-%s-%s", chainA.Config().ChainID, chainB.Config().ChainID) -} - func (p *Chain) IsValoperJailed(ctx context.Context, valoper string) (bool, error) { out, _, err := p.Validators[0].ExecQuery(ctx, "staking", "validator", valoper) if err != nil { diff --git a/tests/interchain/chainsuite/config.go b/tests/interchain/chainsuite/config.go index f67bd2a272f..0ff1ef2f222 100644 --- a/tests/interchain/chainsuite/config.go +++ b/tests/interchain/chainsuite/config.go @@ -49,32 +49,45 @@ const ( ChainSpawnWait = 155 * time.Second SlashingWindowConsumer = 20 BlocksPerDistribution = 10 + StrideVersion = "v22.0.0" + NeutronVersion = "v3.0.2" + TransferPortID = "transfer" + // This is needed because not every ics image is in the default heighliner registry + HyphaICSRepo = "ghcr.io/hyphacoop/ics" + ICSUidGuid = "1025:1025" ) -func (c SuiteConfig) Merge(other SuiteConfig) SuiteConfig { - if c.ChainSpec == nil { - c.ChainSpec = other.ChainSpec - } else if other.ChainSpec != nil { - c.ChainSpec.ChainConfig = c.ChainSpec.MergeChainSpecConfig(other.ChainSpec.ChainConfig) - if other.ChainSpec.Name != "" { - c.ChainSpec.Name = other.ChainSpec.Name - } - if other.ChainSpec.ChainName != "" { - c.ChainSpec.ChainName = other.ChainSpec.ChainName - } - if other.ChainSpec.Version != "" { - c.ChainSpec.Version = other.ChainSpec.Version - } - if other.ChainSpec.NoHostMount != nil { - c.ChainSpec.NoHostMount = other.ChainSpec.NoHostMount - } - if other.ChainSpec.NumValidators != nil { - c.ChainSpec.NumValidators = other.ChainSpec.NumValidators - } - if other.ChainSpec.NumFullNodes != nil { - c.ChainSpec.NumFullNodes = other.ChainSpec.NumFullNodes - } +func MergeChainSpecs(spec, other *interchaintest.ChainSpec) *interchaintest.ChainSpec { + if spec == nil { + return other + } + if other == nil { + return spec + } + spec.ChainConfig = spec.MergeChainSpecConfig(other.ChainConfig) + if other.Name != "" { + spec.Name = other.Name + } + if other.ChainName != "" { + spec.ChainName = other.ChainName + } + if other.Version != "" { + spec.Version = other.Version } + if other.NoHostMount != nil { + spec.NoHostMount = other.NoHostMount + } + if other.NumValidators != nil { + spec.NumValidators = other.NumValidators + } + if other.NumFullNodes != nil { + spec.NumFullNodes = other.NumFullNodes + } + return spec +} + +func (c SuiteConfig) Merge(other SuiteConfig) SuiteConfig { + c.ChainSpec = MergeChainSpecs(c.ChainSpec, other.ChainSpec) c.UpgradeOnSetup = other.UpgradeOnSetup c.CreateRelayer = other.CreateRelayer c.Scope = other.Scope @@ -100,7 +113,7 @@ func DefaultGenesisAmounts(denom string) func(i int) (types.Coin, types.Coin) { } } -func DefaultSuiteConfig(env Environment) SuiteConfig { +func DefaultChainSpec(env Environment) *interchaintest.ChainSpec { fullNodes := 0 validators := ValidatorCount var repository string @@ -109,30 +122,34 @@ func DefaultSuiteConfig(env Environment) SuiteConfig { } else { repository = fmt.Sprintf("%s/%s", env.DockerRegistry, env.GaiaImageName) } - return SuiteConfig{ - ChainSpec: &interchaintest.ChainSpec{ - Name: "gaia", - NumFullNodes: &fullNodes, - NumValidators: &validators, - Version: env.OldGaiaImageVersion, - ChainConfig: ibc.ChainConfig{ - Denom: Uatom, - GasPrices: GasPrices, - GasAdjustment: 2.0, - ConfigFileOverrides: map[string]any{ - "config/config.toml": DefaultConfigToml(), - }, - Images: []ibc.DockerImage{{ - Repository: repository, - UidGid: "1025:1025", // this is the user in heighliner docker images - }}, - ModifyGenesis: cosmos.ModifyGenesis(DefaultGenesis()), - ModifyGenesisAmounts: DefaultGenesisAmounts(Uatom), + return &interchaintest.ChainSpec{ + Name: "gaia", + NumFullNodes: &fullNodes, + NumValidators: &validators, + Version: env.OldGaiaImageVersion, + ChainConfig: ibc.ChainConfig{ + Denom: Uatom, + GasPrices: GasPrices, + GasAdjustment: 2.0, + ConfigFileOverrides: map[string]any{ + "config/config.toml": DefaultConfigToml(), }, + Images: []ibc.DockerImage{{ + Repository: repository, + UidGid: "1025:1025", // this is the user in heighliner docker images + }}, + ModifyGenesis: cosmos.ModifyGenesis(DefaultGenesis()), + ModifyGenesisAmounts: DefaultGenesisAmounts(Uatom), }, } } +func DefaultSuiteConfig(env Environment) SuiteConfig { + return SuiteConfig{ + ChainSpec: DefaultChainSpec(env), + } +} + func DefaultConfigToml() testutil.Toml { configToml := make(testutil.Toml) consensusToml := make(testutil.Toml) diff --git a/tests/interchain/chainsuite/relayer.go b/tests/interchain/chainsuite/relayer.go index 0b5c308603a..26a64d48891 100644 --- a/tests/interchain/chainsuite/relayer.go +++ b/tests/interchain/chainsuite/relayer.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "sort" + "time" "github.com/strangelove-ventures/interchaintest/v8" "github.com/strangelove-ventures/interchaintest/v8/ibc" @@ -41,7 +43,7 @@ func (r *Relayer) SetupChainKeys(ctx context.Context, chain *Chain) error { } func (r *Relayer) GetTransferChannel(ctx context.Context, chain, counterparty *Chain) (*ibc.ChannelOutput, error) { - return r.GetChannelWithPort(ctx, chain, counterparty, "transfer") + return r.GetChannelWithPort(ctx, chain, counterparty, TransferPortID) } func (r *Relayer) GetChannelWithPort(ctx context.Context, chain, counterparty *Chain, portID string) (*ibc.ChannelOutput, error) { @@ -49,37 +51,30 @@ func (r *Relayer) GetChannelWithPort(ctx context.Context, chain, counterparty *C if err != nil { return nil, err } - var client *ibc.ClientOutput for _, c := range clients { if c.ClientState.ChainID == counterparty.Config().ChainID { - client = c - break - } - } - if client == nil { - return nil, fmt.Errorf("no client found for chain %s", counterparty.Config().ChainID) - } - - stdout, _, err := chain.GetNode().ExecQuery(ctx, "ibc", "connection", "connections") - if err != nil { - return nil, fmt.Errorf("error querying connections: %w", err) - } - connections := gjson.GetBytes(stdout, fmt.Sprintf("connections.#(client_id==\"%s\")#.id", client.ClientID)).Array() - if len(connections) == 0 { - return nil, fmt.Errorf("no connections found for client %s", client.ClientID) - } - for _, connID := range connections { - stdout, _, err := chain.GetNode().ExecQuery(ctx, "ibc", "channel", "connections", connID.String()) - if err != nil { - return nil, err - } - channelJSON := gjson.GetBytes(stdout, fmt.Sprintf("channels.#(port_id==\"%s\")", portID)).String() - if channelJSON != "" { - channelOutput := &ibc.ChannelOutput{} - if err := json.Unmarshal([]byte(channelJSON), channelOutput); err != nil { - return nil, fmt.Errorf("error unmarshalling channel output %s: %w", channelJSON, err) + stdout, _, err := chain.GetNode().ExecQuery(ctx, "ibc", "connection", "connections") + if err != nil { + return nil, fmt.Errorf("error querying connections: %w", err) + } + connections := gjson.GetBytes(stdout, fmt.Sprintf("connections.#(client_id==\"%s\")#.id", c.ClientID)).Array() + if len(connections) == 0 { + continue + } + for _, connID := range connections { + stdout, _, err := chain.GetNode().ExecQuery(ctx, "ibc", "channel", "connections", connID.String()) + if err != nil { + return nil, err + } + channelJSON := gjson.GetBytes(stdout, fmt.Sprintf("channels.#(port_id==\"%s\")", portID)).String() + if channelJSON != "" { + channelOutput := &ibc.ChannelOutput{} + if err := json.Unmarshal([]byte(channelJSON), channelOutput); err != nil { + return nil, fmt.Errorf("error unmarshalling channel output %s: %w", channelJSON, err) + } + return channelOutput, nil + } } - return channelOutput, nil } } return nil, fmt.Errorf("no channel found for port %s", portID) @@ -100,3 +95,108 @@ func (r *Relayer) ClearCCVChannel(ctx context.Context, provider, consumer *Chain } return nil } + +func (r *Relayer) ClearTransferChannel(ctx context.Context, chainA, chainB *Chain) error { + channel, err := r.GetTransferChannel(ctx, chainA, chainB) + if err != nil { + return err + } + rs := r.Exec(ctx, GetRelayerExecReporter(ctx), []string{ + "hermes", "clear", "packets", "--port", channel.PortID, "--channel", channel.ChannelID, + "--chain", chainA.Config().ChainID, + }, nil) + if rs.Err != nil { + return fmt.Errorf("error clearing packets: %w", rs.Err) + } + return nil +} + +func (r *Relayer) ConnectProviderConsumer(ctx context.Context, provider *Chain, consumer *Chain) error { + icsPath := relayerICSPathFor(provider, consumer) + rep := GetRelayerExecReporter(ctx) + if err := r.GeneratePath(ctx, rep, consumer.Config().ChainID, provider.Config().ChainID, icsPath); err != nil { + return err + } + + consumerClients, err := r.GetClients(ctx, rep, consumer.Config().ChainID) + if err != nil { + return err + } + sort.Slice(consumerClients, func(i, j int) bool { + return consumerClients[i].ClientID > consumerClients[j].ClientID + }) + var consumerClient *ibc.ClientOutput + for _, client := range consumerClients { + if client.ClientState.ChainID == provider.Config().ChainID { + consumerClient = client + break + } + } + if consumerClient == nil { + return fmt.Errorf("consumer chain %s does not have a client tracking the provider chain %s", consumer.Config().ChainID, provider.Config().ChainID) + } + consumerClientID := consumerClient.ClientID + + providerClients, err := r.GetClients(ctx, rep, provider.Config().ChainID) + if err != nil { + return err + } + sort.Slice(providerClients, func(i, j int) bool { + return providerClients[i].ClientID > providerClients[j].ClientID + }) + + var providerClient *ibc.ClientOutput + for _, client := range providerClients { + if client.ClientState.ChainID == consumer.Config().ChainID { + providerClient = client + break + } + } + if providerClient == nil { + return fmt.Errorf("provider chain %s does not have a client tracking the consumer chain %s for path %s on relayer %s", + provider.Config().ChainID, consumer.Config().ChainID, icsPath, r) + } + providerClientID := providerClient.ClientID + + if err := r.UpdatePath(ctx, rep, icsPath, ibc.PathUpdateOptions{ + SrcClientID: &consumerClientID, + DstClientID: &providerClientID, + }); err != nil { + return err + } + + if err := r.CreateConnections(ctx, rep, icsPath); err != nil { + return err + } + + if err := r.CreateChannel(ctx, rep, icsPath, ibc.CreateChannelOptions{ + SourcePortName: "consumer", + DestPortName: "provider", + Order: ibc.Ordered, + Version: "1", + }); err != nil { + return err + } + + tCtx, tCancel := context.WithTimeout(ctx, 30*CommitTimeout) + defer tCancel() + for tCtx.Err() == nil { + var ch *ibc.ChannelOutput + ch, err = r.GetTransferChannel(ctx, provider, consumer) + if err == nil && ch != nil { + break + } else if err == nil { + err = fmt.Errorf("channel not found") + } + time.Sleep(CommitTimeout) + } + return err +} + +func relayerICSPathFor(chainA, chainB *Chain) string { + return fmt.Sprintf("ics-%s-%s", chainA.Config().ChainID, chainB.Config().ChainID) +} + +func relayerTransferPathFor(chainA, chainB *Chain) string { + return fmt.Sprintf("transfer-%s-%s", chainA.Config().ChainID, chainB.Config().ChainID) +} diff --git a/tests/interchain/consensus_test.go b/tests/interchain/consensus_test.go index 8f09f9f09e4..a256731e4b9 100644 --- a/tests/interchain/consensus_test.go +++ b/tests/interchain/consensus_test.go @@ -6,10 +6,13 @@ import ( "testing" "github.com/cosmos/gaia/v21/tests/interchain/chainsuite" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/ibc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/tidwall/gjson" "github.com/tidwall/sjson" + "golang.org/x/mod/semver" ) const ( @@ -49,15 +52,21 @@ func (s *ConsensusSuite) SetupSuite() { result, err := s.Chain.SubmitProposal(s.GetContext(), s.Chain.ValidatorWallets[0].Moniker, prop) s.Require().NoError(err) s.Require().NoError(s.Chain.PassProposal(s.GetContext(), result.ProposalID)) + s.UpgradeChain() stakingParams, _, err := s.Chain.GetNode().ExecQuery(s.GetContext(), "staking", "params") s.Require().NoError(err) - s.Require().Equal(uint64(200), gjson.GetBytes(stakingParams, "params.max_validators").Uint(), string(stakingParams)) providerParams, _, err := s.Chain.GetNode().ExecQuery(s.GetContext(), "provider", "params") s.Require().NoError(err) - s.Require().Equal(uint64(180), gjson.GetBytes(providerParams, "max_provider_consensus_validators").Uint(), string(providerParams)) + + if semver.Compare(s.Env.OldGaiaImageVersion, "v20.0.0") < 0 { + // These are set by the v20 upgrade handler + s.Require().Equal(uint64(200), gjson.GetBytes(stakingParams, "params.max_validators").Uint(), string(stakingParams)) + s.Require().Equal(uint64(180), gjson.GetBytes(providerParams, "max_provider_consensus_validators").Uint(), string(providerParams)) + } + providerParams, err = sjson.SetBytes(providerParams, "max_provider_consensus_validators", maxProviderConsensusValidators) s.Require().NoError(err) providerProposal, err := sjson.SetRaw(fmt.Sprintf(`{ @@ -84,12 +93,23 @@ func (s *ConsensusSuite) SetupSuite() { cfg := chainsuite.ConsumerConfig{ ChainName: "ics-consumer", - Version: "v5.0.0", + Version: "v6.2.1", ShouldCopyProviderKey: allProviderKeysCopied(), Denom: chainsuite.Ucon, TopN: 100, AllowInactiveVals: true, MinStake: 1_000_000, + Spec: &interchaintest.ChainSpec{ + ChainConfig: ibc.ChainConfig{ + Images: []ibc.DockerImage{ + { + Repository: chainsuite.HyphaICSRepo, + Version: "v6.2.1", + UidGid: chainsuite.ICSUidGuid, + }, + }, + }, + }, } consumer, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg) s.Require().NoError(err) @@ -109,7 +129,7 @@ func (s *ConsensusSuite) Test0ValidatorSets() { s.Require().Equal(s.Chain.ValidatorWallets[i].ValConsAddress, valCons) } - vals, err = s.Consumer.QueryJSON(s.GetContext(), "validators", "comet-validator-set") + vals, err = s.Consumer.QueryJSON(s.GetContext(), "validators", "tendermint-validator-set") s.Require().NoError(err) s.Require().Equal(maxProviderConsensusValidators, len(vals.Array()), vals) for i := 0; i < maxProviderConsensusValidators; i++ { @@ -161,7 +181,7 @@ func (s *ConsensusSuite) TestOptInInactive() { s.Require().NoError(err) s.Relayer.ClearCCVChannel(s.GetContext(), s.Chain, s.Consumer) s.Require().EventuallyWithT(func(c *assert.CollectT) { - vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "comet-validator-set") + vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "tendermint-validator-set") assert.NoError(c, err) assert.Equal(c, maxProviderConsensusValidators, len(vals.Array()), vals) }, 10*chainsuite.CommitTimeout, chainsuite.CommitTimeout) @@ -171,7 +191,7 @@ func (s *ConsensusSuite) TestOptInInactive() { }() s.Require().NoError(s.Relayer.ClearCCVChannel(s.GetContext(), s.Chain, s.Consumer)) s.Require().EventuallyWithT(func(c *assert.CollectT) { - vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "comet-validator-set") + vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "tendermint-validator-set") assert.NoError(c, err) assert.Equal(c, maxProviderConsensusValidators+1, len(vals.Array()), vals) }, 10*chainsuite.CommitTimeout, chainsuite.CommitTimeout) @@ -182,7 +202,7 @@ func (s *ConsensusSuite) TestOptInInactive() { _, err = s.Chain.Validators[5].ExecTx(s.GetContext(), s.Chain.ValidatorWallets[5].Moniker, "provider", "opt-in", consumerID) s.Require().NoError(err) s.Require().NoError(s.Relayer.ClearCCVChannel(s.GetContext(), s.Chain, s.Consumer)) - vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "comet-validator-set") + vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "tendermint-validator-set") s.Require().NoError(err) s.Require().Equal(maxProviderConsensusValidators+1, len(vals.Array()), vals) jailed, err = s.Chain.IsValidatorJailedForConsumerDowntime(s.GetContext(), s.Relayer, s.Consumer, 5) diff --git a/tests/interchain/consumer_launch_test.go b/tests/interchain/consumer_launch_test.go index 8cf9204f9ab..ae4c343adc8 100644 --- a/tests/interchain/consumer_launch_test.go +++ b/tests/interchain/consumer_launch_test.go @@ -3,16 +3,20 @@ package interchain_test import ( "testing" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/ibc" "github.com/stretchr/testify/suite" + "golang.org/x/mod/semver" "github.com/cosmos/gaia/v21/tests/interchain/chainsuite" ) type ConsumerLaunchSuite struct { *chainsuite.Suite - OtherChain string - OtherChainVersion string - ShouldCopyProviderKey [chainsuite.ValidatorCount]bool + OtherChain string + OtherChainVersionPreUpgrade string + OtherChainVersionPostUpgrade string + ShouldCopyProviderKey [chainsuite.ValidatorCount]bool } func noProviderKeysCopied() [chainsuite.ValidatorCount]bool { @@ -30,10 +34,21 @@ func someProviderKeysCopied() [chainsuite.ValidatorCount]bool { func (s *ConsumerLaunchSuite) TestChainLaunch() { cfg := chainsuite.ConsumerConfig{ ChainName: s.OtherChain, - Version: s.OtherChainVersion, + Version: s.OtherChainVersionPreUpgrade, ShouldCopyProviderKey: s.ShouldCopyProviderKey, Denom: chainsuite.Ucon, TopN: 94, + Spec: &interchaintest.ChainSpec{ + ChainConfig: ibc.ChainConfig{ + Images: []ibc.DockerImage{ + { + Repository: chainsuite.HyphaICSRepo, + Version: s.OtherChainVersionPreUpgrade, + UidGid: chainsuite.ICSUidGuid, + }, + }, + }, + }, } consumer, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg) s.Require().NoError(err) @@ -53,6 +68,8 @@ func (s *ConsumerLaunchSuite) TestChainLaunch() { s.Require().NoError(err) s.Require().False(jailed, "validator 5 should not be jailed for downtime") + cfg.Version = s.OtherChainVersionPostUpgrade + cfg.Spec.ChainConfig.Images[0].Version = s.OtherChainVersionPostUpgrade consumer2, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg) s.Require().NoError(err) err = s.Chain.CheckCCV(s.GetContext(), consumer2, s.Relayer, 1_000_000, 0, 1) @@ -67,42 +84,53 @@ func (s *ConsumerLaunchSuite) TestChainLaunch() { s.Require().False(jailed, "validator 5 should not be jailed for downtime") } -func TestICS40ChainLaunch(t *testing.T) { +func selectConsumerVersion(preV21, postV21 string) string { + if semver.Compare(semver.Major(chainsuite.GetEnvironment().OldGaiaImageVersion), "v21") >= 0 { + return postV21 + } + return preV21 +} + +func TestICS4ChainLaunch(t *testing.T) { s := &ConsumerLaunchSuite{ - Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}), - OtherChain: "ics-consumer", - OtherChainVersion: "v4.0.0", - ShouldCopyProviderKey: noProviderKeysCopied(), + Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}), + OtherChain: "ics-consumer", + OtherChainVersionPreUpgrade: selectConsumerVersion("v4.4.1", "v4.5.0"), + OtherChainVersionPostUpgrade: "v4.5.0", + ShouldCopyProviderKey: noProviderKeysCopied(), } suite.Run(t, s) } -func TestICS33ConsumerAllKeysChainLaunch(t *testing.T) { +func TestICS6ConsumerAllKeysChainLaunch(t *testing.T) { s := &ConsumerLaunchSuite{ - Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}), - OtherChain: "ics-consumer", - OtherChainVersion: "v3.3.0", - ShouldCopyProviderKey: allProviderKeysCopied(), + Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}), + OtherChain: "ics-consumer", + OtherChainVersionPreUpgrade: selectConsumerVersion("v6.0.0", "v6.2.1"), + OtherChainVersionPostUpgrade: "v6.2.1", + ShouldCopyProviderKey: allProviderKeysCopied(), } suite.Run(t, s) } -func TestICS33ConsumerSomeKeysChainLaunch(t *testing.T) { +func TestICS6ConsumerSomeKeysChainLaunch(t *testing.T) { s := &ConsumerLaunchSuite{ - Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}), - OtherChain: "ics-consumer", - OtherChainVersion: "v3.3.0", - ShouldCopyProviderKey: someProviderKeysCopied(), + Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}), + OtherChain: "ics-consumer", + OtherChainVersionPreUpgrade: selectConsumerVersion("v6.0.0", "v6.2.1"), + OtherChainVersionPostUpgrade: "v6.2.1", + ShouldCopyProviderKey: someProviderKeysCopied(), } suite.Run(t, s) } -func TestICS33ConsumerNoKeysChainLaunch(t *testing.T) { +func TestICS6ConsumerNoKeysChainLaunch(t *testing.T) { s := &ConsumerLaunchSuite{ - Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}), - OtherChain: "ics-consumer", - OtherChainVersion: "v3.3.0", - ShouldCopyProviderKey: noProviderKeysCopied(), + Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}), + OtherChain: "ics-consumer", + OtherChainVersionPreUpgrade: selectConsumerVersion("v6.0.0", "v6.2.1"), + OtherChainVersionPostUpgrade: "v6.2.1", + ShouldCopyProviderKey: noProviderKeysCopied(), } suite.Run(t, s) } @@ -112,12 +140,13 @@ type MainnetConsumerChainsSuite struct { } func (s *MainnetConsumerChainsSuite) TestMainnetConsumerChainsAfterUpgrade() { - const neutronVersion = "v3.0.2" - const strideVersion = "v22.0.0" - + // We can't do these consumer launches yet because the chains aren't compatible with launching on v21 yet + if semver.Major(s.Env.OldGaiaImageVersion) == s.Env.UpgradeName && s.Env.UpgradeName == "v21" { + s.T().Skip("Skipping Consumer Launch tests when going from v21 -> v21") + } neutron, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, chainsuite.ConsumerConfig{ ChainName: "neutron", - Version: neutronVersion, + Version: chainsuite.NeutronVersion, ShouldCopyProviderKey: allProviderKeysCopied(), Denom: chainsuite.NeutronDenom, TopN: 95, @@ -125,7 +154,7 @@ func (s *MainnetConsumerChainsSuite) TestMainnetConsumerChainsAfterUpgrade() { s.Require().NoError(err) stride, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, chainsuite.ConsumerConfig{ ChainName: "stride", - Version: strideVersion, + Version: chainsuite.StrideVersion, ShouldCopyProviderKey: allProviderKeysCopied(), Denom: chainsuite.StrideDenom, TopN: 95, diff --git a/tests/interchain/feemarket_test.go b/tests/interchain/feemarket_test.go new file mode 100644 index 00000000000..473915832ac --- /dev/null +++ b/tests/interchain/feemarket_test.go @@ -0,0 +1,221 @@ +package interchain_test + +import ( + "encoding/json" + "fmt" + "path" + "strconv" + "testing" + "time" + + "github.com/cosmos/gaia/v21/tests/interchain/chainsuite" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/testutil" + "github.com/stretchr/testify/suite" + "github.com/tidwall/sjson" + "golang.org/x/sync/errgroup" +) + +type FeemarketSuite struct { + *chainsuite.Suite +} + +func (s *FeemarketSuite) TestGasGoesUp() { + const ( + txsPerBlock = 600 + blocksToPack = 5 + maxBlockUtilization = 1000000 + ) + + s.setMaxBlockUtilization(maxBlockUtilization) + + s.setCommitTimeout(150 * time.Second) + + s.packBlocks(txsPerBlock, blocksToPack) +} + +func (s *FeemarketSuite) setMaxBlockUtilization(utilization int) { + params, _, err := s.Chain.GetNode().ExecQuery(s.GetContext(), "feemarket", "params") + s.Require().NoError(err) + + params, err = sjson.SetBytes(params, "max_block_utilization", fmt.Sprint(utilization)) + s.Require().NoError(err) + + govAuthority, err := s.Chain.GetGovernanceAddress(s.GetContext()) + s.Require().NoError(err) + + proposalJson := fmt.Sprintf(`{ + "@type": "/feemarket.feemarket.v1.MsgParams", + "authority": "%s" +}`, govAuthority) + proposalJson, err = sjson.SetRaw(proposalJson, "params", string(params)) + s.Require().NoError(err) + + txhash, err := s.Chain.GetNode().SubmitProposal(s.GetContext(), interchaintest.FaucetAccountKeyName, + cosmos.TxProposalv1{ + Title: "Set Block Params", + Deposit: chainsuite.GovDepositAmount, + Messages: []json.RawMessage{json.RawMessage(proposalJson)}, + Summary: "Set Block Params", + Metadata: "ipfs://CID", + }) + s.Require().NoError(err) + + propId, err := s.Chain.GetProposalID(s.GetContext(), txhash) + s.Require().NoError(err) + s.Require().NoError(s.Chain.PassProposal(s.GetContext(), propId)) + maxBlockResult, err := s.Chain.QueryJSON(s.GetContext(), "max_block_utilization", "feemarket", "params") + s.Require().NoError(err) + maxBlock := maxBlockResult.String() + s.Require().Equal(fmt.Sprint(utilization), maxBlock) + +} + +func (s *FeemarketSuite) packBlocks(txsPerBlock, blocksToPack int) { + script := ` +#!/bin/sh + +set -ue +set -o pipefail + +TX_COUNT=$1 +CHAIN_BINARY=$2 +FROM=$3 +TO=$4 +DENOM=$5 +GAS_PRICES=$6 +CHAIN_ID=$7 +VAL_HOME=$8 +NODE=$9 + +i=0 + + +cd $HOME + +SEQUENCE=$($CHAIN_BINARY query auth account $FROM --chain-id $CHAIN_ID --node $NODE --home $VAL_HOME -o json | jq -r .account.value.sequence) +ACCOUNT=$($CHAIN_BINARY query auth account $FROM --chain-id $CHAIN_ID --node $NODE --home $VAL_HOME -o json | jq -r .account.value.account_number) + +if [ $SEQUENCE == "null" ]; then + $CHAIN_BINARY query auth account $FROM --chain-id $CHAIN_ID --node $NODE --home $VAL_HOME -o json >&2 + exit 1 +fi + +if [ $ACCOUNT == "null" ]; then + ACCOUNT=0 +fi + +$CHAIN_BINARY tx bank send $FROM $TO 1$DENOM --keyring-backend test --generate-only --account-number $ACCOUNT --from $FROM --chain-id $CHAIN_ID --gas 500000 --gas-adjustment 2.0 --gas-prices $GAS_PRICES$DENOM --home $VAL_HOME --node $NODE -o json > tx.json + +while [ $i -lt $TX_COUNT ]; do + $CHAIN_BINARY tx sign tx.json --from $FROM --chain-id $CHAIN_ID --sequence $SEQUENCE --keyring-backend test --account-number $ACCOUNT --offline --home $VAL_HOME > tx.json.signed + tx=$($CHAIN_BINARY tx broadcast tx.json.signed --node $NODE --chain-id $CHAIN_ID --home $VAL_HOME -o json) + if [ $(echo $tx | jq -r .code) -ne 0 ]; then + echo "$tx" >&2 + $CHAIN_BINARY query tx $(echo $tx | jq -r .txhash) --chain-id $CHAIN_ID --node $NODE --home $VAL_HOME >&2 + exit 1 + else + echo $(echo $tx | jq -r .txhash) + fi + SEQUENCE=$((SEQUENCE+1)) + i=$((i+1)) +done +` + for _, val := range s.Chain.Validators { + err := val.WriteFile(s.GetContext(), []byte(script), "pack.sh") + s.Require().NoError(err) + } + wallets := s.Chain.ValidatorWallets + + gasResult, err := s.Chain.QueryJSON(s.GetContext(), "price.amount", "feemarket", "gas-price", s.Chain.Config().Denom) + s.Require().NoError(err) + gasStr := gasResult.String() + gasBefore, err := strconv.ParseFloat(gasStr, 64) + s.Require().NoError(err) + gasNow := gasBefore + + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 1, s.Chain)) + + prevBlock, err := s.Chain.Height(s.GetContext()) + s.Require().NoError(err) + + for i := 0; i < blocksToPack; i++ { + eg := errgroup.Group{} + for v, val := range s.Chain.Validators { + val := val + v := v + eg.Go(func() error { + _, stderr, err := val.Exec(s.GetContext(), []string{ + "sh", path.Join(val.HomeDir(), "pack.sh"), + strconv.Itoa(txsPerBlock / len(s.Chain.Validators)), + s.Chain.Config().Bin, + wallets[v].Address, + wallets[(v+1)%len(s.Chain.Validators)].Address, + s.Chain.Config().Denom, + fmt.Sprint(gasNow), + s.Chain.Config().ChainID, + val.HomeDir(), + fmt.Sprintf("tcp://%s:26657", val.HostName()), + }, nil) + + if err != nil { + return fmt.Errorf("validator %d, err %w, stderr: %s", v, err, stderr) + } else if len(stderr) > 0 { + return fmt.Errorf("stderr: %s", stderr) + } + return nil + }) + } + s.Require().NoError(eg.Wait()) + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 1, s.Chain)) + time.Sleep(5 * time.Second) // ensure the feemarket has time to update + currentBlock, err := s.Chain.Height(s.GetContext()) + s.Require().NoError(err) + s.Require().Equal(prevBlock+1, currentBlock) + prevBlock = currentBlock + + gasResult, err := s.Chain.QueryJSON(s.GetContext(), "price.amount", "feemarket", "gas-price", s.Chain.Config().Denom) + s.Require().NoError(err) + gasStr = gasResult.String() + gasNow, err = strconv.ParseFloat(gasStr, 64) + s.Require().NoError(err) + s.Require().Greater(gasNow, gasBefore) + gasBefore = gasNow + } +} + +func (s *FeemarketSuite) setCommitTimeout(timeout time.Duration) { + eg := errgroup.Group{} + for _, val := range s.Chain.Validators { + val := val + eg.Go(func() error { + configToml := make(testutil.Toml) + consensusToml := make(testutil.Toml) + consensusToml["timeout_commit"] = timeout.String() + configToml["consensus"] = consensusToml + if err := testutil.ModifyTomlConfigFile( + s.GetContext(), chainsuite.GetLogger(s.GetContext()), + val.DockerClient, s.T().Name(), val.VolumeName, + "config/config.toml", configToml, + ); err != nil { + return err + } + if err := val.StopContainer(s.GetContext()); err != nil { + return err + } + return val.StartContainer(s.GetContext()) + }) + } + s.Require().NoError(eg.Wait()) + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 1, s.Chain)) +} + +func TestFeemarket(t *testing.T) { + s := &FeemarketSuite{ + Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{ + UpgradeOnSetup: true, + }), + } + suite.Run(t, s) +} diff --git a/tests/interchain/go.mod b/tests/interchain/go.mod index b5355dc503d..d116fea5d8c 100644 --- a/tests/interchain/go.mod +++ b/tests/interchain/go.mod @@ -23,6 +23,7 @@ require ( cosmossdk.io/math v1.3.0 github.com/cometbft/cometbft v0.38.11 github.com/cosmos/cosmos-sdk v0.50.9 + github.com/cosmos/gogoproto v1.7.0 github.com/cosmos/ibc-go/v8 v8.5.0 github.com/cosmos/interchain-security/v5 v5.1.1 github.com/docker/docker v27.1.2+incompatible @@ -95,7 +96,6 @@ require ( github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect - github.com/cosmos/gogoproto v1.7.0 // indirect github.com/cosmos/iavl v1.3.0 // indirect github.com/cosmos/ibc-go/modules/capability v1.0.1 // indirect github.com/cosmos/ics23/go v0.11.0 // indirect @@ -292,4 +292,4 @@ require ( sigs.k8s.io/yaml v1.4.0 // indirect ) -replace github.com/strangelove-ventures/interchaintest/v8 => github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20240904201357-3a54d751e08d +replace github.com/strangelove-ventures/interchaintest/v8 => github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20241007153747-ed0a63d6cc1c diff --git a/tests/interchain/go.sum b/tests/interchain/go.sum index 827bcf254c0..b2d2be54de8 100644 --- a/tests/interchain/go.sum +++ b/tests/interchain/go.sum @@ -773,8 +773,8 @@ github.com/huandu/skiplist v1.2.0/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXM github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= -github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20240904201357-3a54d751e08d h1:3LXY5EWY78Qxh2t4h2rtcm/XpJdryN7bML2Nb0VfUjc= -github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20240904201357-3a54d751e08d/go.mod h1:/4eZW5g+Gm5E7fCJvNVyjSlEyFkAfMzap4i8E6iqyyU= +github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20241007153747-ed0a63d6cc1c h1:zTsxQIsnbocbpjqA6yEpM7nYYrqlF7EjWWFgHUEQtCE= +github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20241007153747-ed0a63d6cc1c/go.mod h1:/4eZW5g+Gm5E7fCJvNVyjSlEyFkAfMzap4i8E6iqyyU= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= diff --git a/tests/interchain/ica_controller_test.go b/tests/interchain/ica_controller_test.go new file mode 100644 index 00000000000..3e972bfcd4e --- /dev/null +++ b/tests/interchain/ica_controller_test.go @@ -0,0 +1,148 @@ +package interchain_test + +import ( + "context" + "encoding/json" + "strings" + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/gaia/v21/tests/interchain/chainsuite" + "github.com/cosmos/gogoproto/proto" + icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type ICAControllerSuite struct { + *chainsuite.Suite + Host *chainsuite.Chain +} + +func (s *ICAControllerSuite) SetupSuite() { + s.Suite.SetupSuite() + host, err := s.Chain.AddLinkedChain(s.GetContext(), s.T(), s.Relayer, chainsuite.DefaultChainSpec(s.Env)) + s.Require().NoError(err) + s.Host = host +} + +func (s *ICAControllerSuite) TestICAController() { + const amountToSend = int64(3_300_000_000) + wallets := s.Chain.ValidatorWallets + valIdx := 0 + + var icaAddress, srcAddress string + var err error + for ; valIdx < len(wallets); valIdx++ { + srcAddress = wallets[valIdx].Address + icaAddress, err = s.Chain.SetupICAAccount(s.GetContext(), s.Host, s.Relayer, srcAddress, valIdx, amountToSend) + if err == nil { + break + } else if strings.Contains(err.Error(), "active channel already set for this owner") { + chainsuite.GetLogger(s.GetContext()).Sugar().Warnf("error setting up ICA account: %s", err) + valIdx++ + continue + } + // if we get here, fail the test. Unexpected error. + s.Require().NoError(err) + } + if icaAddress == "" { + // this'll happen if every validator has an ICA account already + s.Require().Fail("unable to create ICA account") + } + + srcChannel, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chain, s.Host) + s.Require().NoError(err) + + _, err = s.Chain.SendIBCTransfer(s.GetContext(), srcChannel.ChannelID, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Address: icaAddress, + Amount: sdkmath.NewInt(amountToSend), + Denom: s.Chain.Config().Denom, + }, ibc.TransferOptions{}) + s.Require().NoError(err) + + wallets = s.Host.ValidatorWallets + s.Require().NoError(err) + dstAddress := wallets[0].Address + + var ibcStakeDenom string + s.Require().EventuallyWithT(func(c *assert.CollectT) { + balances, err := s.Host.BankQueryAllBalances(s.GetContext(), icaAddress) + s.Require().NoError(err) + s.Require().NotEmpty(balances) + for _, c := range balances { + if strings.Contains(c.Denom, "ibc") { + ibcStakeDenom = c.Denom + break + } + } + assert.NotEmpty(c, ibcStakeDenom) + }, 10*chainsuite.CommitTimeout, chainsuite.CommitTimeout) + + recipientBalanceBefore, err := s.Host.GetBalance(s.GetContext(), dstAddress, ibcStakeDenom) + s.Require().NoError(err) + + icaAmount := int64(amountToSend / 3) + + srcConnection := srcChannel.ConnectionHops[0] + + s.Require().NoError(s.sendICATx(s.GetContext(), valIdx, srcAddress, dstAddress, icaAddress, srcConnection, icaAmount, ibcStakeDenom)) + + s.Require().EventuallyWithT(func(c *assert.CollectT) { + recipientBalanceAfter, err := s.Host.GetBalance(s.GetContext(), dstAddress, ibcStakeDenom) + assert.NoError(c, err) + + assert.Equal(c, recipientBalanceBefore.Add(sdkmath.NewInt(icaAmount)), recipientBalanceAfter) + }, 10*chainsuite.CommitTimeout, chainsuite.CommitTimeout) + +} + +func TestICAController(t *testing.T) { + s := &ICAControllerSuite{Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{ + UpgradeOnSetup: true, + CreateRelayer: true, + })} + suite.Run(t, s) +} + +func (s *ICAControllerSuite) sendICATx(ctx context.Context, valIdx int, srcAddress string, dstAddress string, icaAddress string, srcConnection string, amount int64, denom string) error { + interfaceRegistry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(interfaceRegistry) + + bankSendMsg := banktypes.NewMsgSend( + sdk.MustAccAddressFromBech32(icaAddress), + sdk.MustAccAddressFromBech32(dstAddress), + sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(amount))), + ) + data, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{bankSendMsg}, icatypes.EncodingProtobuf) + if err != nil { + return err + } + + msg, err := json.Marshal(icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: data, + }) + if err != nil { + return err + } + msgPath := "msg.json" + if err := s.Chain.Validators[valIdx].WriteFile(ctx, msg, msgPath); err != nil { + return err + } + msgPath = s.Chain.Validators[valIdx].HomeDir() + "/" + msgPath + _, err = s.Chain.Validators[valIdx].ExecTx(ctx, srcAddress, + "interchain-accounts", "controller", "send-tx", + srcConnection, msgPath, + ) + if err != nil { + return err + } + return nil +} diff --git a/tests/interchain/lsm_test.go b/tests/interchain/lsm_test.go new file mode 100644 index 00000000000..d99d5d3e482 --- /dev/null +++ b/tests/interchain/lsm_test.go @@ -0,0 +1,493 @@ +package interchain_test + +import ( + "encoding/json" + "fmt" + "path" + "testing" + "time" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/gaia/v21/tests/interchain/chainsuite" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "golang.org/x/mod/semver" + "golang.org/x/sync/errgroup" +) + +const ( + lsmBondingMoniker = "bonding" + lsmLiquid1Moniker = "liquid_1" + lsmLiquid2Moniker = "liquid_2" + lsmLiquid3Moniker = "liquid_3" + lsmOwnerMoniker = "owner" +) + +type LSMSuite struct { + *chainsuite.Suite + Stride *chainsuite.Chain + ICAAddr string + LSMWallets map[string]ibc.Wallet + ShareFactor sdkmath.Int +} + +func (s *LSMSuite) checkAMinusBEqualsX(a, b string, x sdkmath.Int) { + intA, err := chainsuite.StrToSDKInt(a) + s.Require().NoError(err) + intB, err := chainsuite.StrToSDKInt(b) + s.Require().NoError(err) + s.Require().True(intA.Sub(intB).Equal(x), "a - b = %s, expected %s", intA.Sub(intB).String(), x.String()) +} + +func (s *LSMSuite) TestLSMHappyPath() { + const ( + delegation = 100000000 + tokenize = 50000000 + bankSend = 20000000 + ibcTransfer = 10000000 + liquid1Redeem = 20000000 + ) + providerWallet := s.Chain.ValidatorWallets[0] + + strideWallet := s.Stride.ValidatorWallets[0] + + s.Run("Validator Bond", func() { + delegatorShares1, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + validatorBondShares1, err := s.Chain.QueryJSON(s.GetContext(), "validator.validator_bond_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmBondingMoniker].FormattedAddress(), + "staking", "delegate", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", delegation, s.Chain.Config().Denom)) + s.Require().NoError(err) + delegatorShares2, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + s.checkAMinusBEqualsX(delegatorShares2.String(), delegatorShares1.String(), sdkmath.NewInt(delegation).Mul(s.ShareFactor)) + + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmBondingMoniker].FormattedAddress(), + "staking", "validator-bond", providerWallet.ValoperAddress) + s.Require().NoError(err) + validatorBondShares2, err := s.Chain.QueryJSON(s.GetContext(), "validator.validator_bond_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + s.checkAMinusBEqualsX(validatorBondShares2.String(), validatorBondShares1.String(), sdkmath.NewInt(delegation).Mul(s.ShareFactor)) + }) + + var tokenizedDenom string + s.Run("Tokenize", func() { + delegatorShares1, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(), + "staking", "delegate", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", delegation, s.Chain.Config().Denom)) + s.Require().NoError(err) + delegatorShares2, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + s.checkAMinusBEqualsX(delegatorShares2.String(), delegatorShares1.String(), sdkmath.NewInt(delegation).Mul(s.ShareFactor)) + + sharesPreTokenize, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(), + "staking", "tokenize-share", + providerWallet.ValoperAddress, fmt.Sprintf("%d%s", tokenize, s.Chain.Config().Denom), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(), + "--gas", "auto") + s.Require().NoError(err) + sharesPostTokenize, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + s.checkAMinusBEqualsX(sharesPostTokenize.String(), sharesPreTokenize.String(), sdkmath.NewInt(tokenize).Mul(s.ShareFactor)) + + balances, err := s.Chain.BankQueryAllBalances(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress()) + s.Require().NoError(err) + for _, balance := range balances { + if balance.Amount.Int64() == tokenize { + tokenizedDenom = balance.Denom + } + } + s.Require().NotEmpty(tokenizedDenom) + }) + + s.Run("Transfer Ownership", func() { + recordIDResult, err := s.Chain.QueryJSON(s.GetContext(), "record.id", "staking", "tokenize-share-record-by-denom", tokenizedDenom) + s.Require().NoError(err) + recordID := recordIDResult.String() + + ownerResult, err := s.Chain.QueryJSON(s.GetContext(), "record.owner", "staking", "tokenize-share-record-by-denom", tokenizedDenom) + s.Require().NoError(err) + owner := ownerResult.String() + + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), owner, + "staking", "transfer-tokenize-share-record", recordID, s.LSMWallets[lsmOwnerMoniker].FormattedAddress()) + s.Require().NoError(err) + + ownerResult, err = s.Chain.QueryJSON(s.GetContext(), "record.owner", "staking", "tokenize-share-record-by-denom", tokenizedDenom) + s.Require().NoError(err) + owner = ownerResult.String() + s.Require().Equal(s.LSMWallets[lsmOwnerMoniker].FormattedAddress(), owner) + + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), owner, + "staking", "transfer-tokenize-share-record", recordID, s.LSMWallets[lsmLiquid1Moniker].FormattedAddress()) + s.Require().NoError(err) + + ownerResult, err = s.Chain.QueryJSON(s.GetContext(), "record.owner", "staking", "tokenize-share-record-by-denom", tokenizedDenom) + s.Require().NoError(err) + owner = ownerResult.String() + s.Require().Equal(s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(), owner) + }) + + var happyLiquid1Delegations1 string + var ibcDenom string + + ibcChannelProvider, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chain, s.Stride) + s.Require().NoError(err) + ibcChannelStride, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Stride, s.Chain) + s.Require().NoError(err) + + s.Run("Transfer Tokens", func() { + happyLiquid1Delegations1Result, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").delegation.shares", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid1Moniker].FormattedAddress()) + s.Require().NoError(err) + happyLiquid1Delegations1 = happyLiquid1Delegations1Result.String() + + err = s.Chain.SendFunds(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(), ibc.WalletAmount{ + Amount: sdkmath.NewInt(bankSend), + Denom: tokenizedDenom, + Address: s.LSMWallets[lsmLiquid2Moniker].FormattedAddress(), + }) + s.Require().NoError(err) + + _, err = s.Chain.SendIBCTransfer(s.GetContext(), ibcChannelProvider.ChannelID, s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(), ibc.WalletAmount{ + Amount: sdkmath.NewInt(ibcTransfer), + Denom: tokenizedDenom, + Address: strideWallet.Address, + }, ibc.TransferOptions{}) + s.Require().NoError(err) + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 5, s.Stride)) + balances, err := s.Stride.BankQueryAllBalances(s.GetContext(), strideWallet.Address) + s.Require().NoError(err) + for _, balance := range balances { + if balance.Amount.Int64() == ibcTransfer { + ibcDenom = balance.Denom + } + } + s.Require().NotEmpty(ibcDenom) + }) + + var happyLiquid1DelegationBalance string + s.Run("Redeem Tokens", func() { + _, err := s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(), + "staking", "redeem-tokens", fmt.Sprintf("%d%s", liquid1Redeem, tokenizedDenom), + "--gas", "auto") + s.Require().NoError(err) + + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid2Moniker].FormattedAddress(), + "staking", "redeem-tokens", fmt.Sprintf("%d%s", bankSend, tokenizedDenom), + "--gas", "auto") + s.Require().NoError(err) + + _, err = s.Stride.SendIBCTransfer(s.GetContext(), ibcChannelStride.ChannelID, strideWallet.Address, ibc.WalletAmount{ + Amount: sdkmath.NewInt(ibcTransfer), + Denom: ibcDenom, + Address: s.LSMWallets[lsmLiquid3Moniker].FormattedAddress(), + }, ibc.TransferOptions{}) + s.Require().NoError(err) + // wait for the transfer to be reflected + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 5, s.Chain)) + + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid3Moniker].FormattedAddress(), + "staking", "redeem-tokens", fmt.Sprintf("%d%s", ibcTransfer, tokenizedDenom), + "--gas", "auto") + s.Require().NoError(err) + + happyLiquid1Delegations2Result, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").delegation.shares", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid1Moniker].FormattedAddress()) + s.Require().NoError(err) + happyLiquid1Delegations2 := happyLiquid1Delegations2Result.String() + s.checkAMinusBEqualsX(happyLiquid1Delegations2, happyLiquid1Delegations1, sdkmath.NewInt(liquid1Redeem).Mul(s.ShareFactor)) + + happyLiquid2DelegationsResult, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").delegation.shares", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid2Moniker].FormattedAddress()) + s.Require().NoError(err) + happyLiquid2Delegations := happyLiquid2DelegationsResult.String() + // LOL there are better ways of doing this + s.checkAMinusBEqualsX(happyLiquid2Delegations, "0", sdkmath.NewInt(bankSend).Mul(s.ShareFactor)) + + happyLiquid3DelegationsResult, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").delegation.shares", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid3Moniker].FormattedAddress()) + s.Require().NoError(err) + happyLiquid3Delegations := happyLiquid3DelegationsResult.String() + s.checkAMinusBEqualsX(happyLiquid3Delegations, "0", sdkmath.NewInt(ibcTransfer).Mul(s.ShareFactor)) + + happyLiquid1DelegationBalanceResult, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").balance.amount", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid1Moniker].FormattedAddress()) + s.Require().NoError(err) + happyLiquid1DelegationBalance = happyLiquid1DelegationBalanceResult.String() + + happyLiquid2DelegationBalanceResult, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").balance.amount", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid2Moniker].FormattedAddress()) + s.Require().NoError(err) + happyLiquid2DelegationBalance := happyLiquid2DelegationBalanceResult.String() + + happyLiquid3DelegationBalanceResult, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").balance.amount", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid3Moniker].FormattedAddress()) + s.Require().NoError(err) + happyLiquid3DelegationBalance := happyLiquid3DelegationBalanceResult.String() + + s.checkAMinusBEqualsX(happyLiquid1DelegationBalance, "0", sdkmath.NewInt(70000000)) + s.checkAMinusBEqualsX(happyLiquid2DelegationBalance, "0", sdkmath.NewInt(bankSend)) + s.checkAMinusBEqualsX(happyLiquid3DelegationBalance, "0", sdkmath.NewInt(ibcTransfer)) + }) + s.Run("Cleanup", func() { + _, err := s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmBondingMoniker].FormattedAddress(), + "staking", "unbond", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", delegation, s.Chain.Config().Denom)) + s.Require().NoError(err) + + validatorBondSharesResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.validator_bond_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + validatorBondShares := validatorBondSharesResult.String() + s.checkAMinusBEqualsX(validatorBondShares, "0", sdkmath.NewInt(0).Mul(s.ShareFactor)) + + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(), + "staking", "unbond", providerWallet.ValoperAddress, fmt.Sprintf("%s%s", happyLiquid1DelegationBalance, s.Chain.Config().Denom)) + s.Require().NoError(err) + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid2Moniker].FormattedAddress(), + "staking", "unbond", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", bankSend, s.Chain.Config().Denom)) + s.Require().NoError(err) + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid3Moniker].FormattedAddress(), + "staking", "unbond", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", ibcTransfer, s.Chain.Config().Denom)) + s.Require().NoError(err) + }) +} + +func (s *LSMSuite) TestICADelegate() { + const ( + delegate = 20000000 + bondDelegation = 20000000 + ) + bondingWallet, err := s.Chain.BuildWallet(s.GetContext(), fmt.Sprintf("lsm_happy_bonding_%d", time.Now().Unix()), "") + s.Require().NoError(err) + + err = s.Chain.SendFunds(s.GetContext(), interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Amount: sdkmath.NewInt(50_000_000), + Denom: s.Chain.Config().Denom, + Address: bondingWallet.FormattedAddress(), + }) + s.Require().NoError(err) + + providerWallet := s.Chain.ValidatorWallets[1] + + strideWallet := s.Stride.ValidatorWallets[0] + + s.Run("Delegate and Bond", func() { + shares1Result, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + shares1 := shares1Result.String() + + tokens1Result, err := s.Chain.QueryJSON(s.GetContext(), "validator.tokens", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + tokens1 := tokens1Result.String() + + bondShares1Result, err := s.Chain.QueryJSON(s.GetContext(), "validator.validator_bond_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + bondShares1 := bondShares1Result.String() + + shares1Int, err := chainsuite.StrToSDKInt(shares1) + s.Require().NoError(err) + tokens1Int, err := chainsuite.StrToSDKInt(tokens1) + s.Require().NoError(err) + bondShares1Int, err := chainsuite.StrToSDKInt(bondShares1) + s.Require().NoError(err) + + exchangeRate1 := shares1Int.Quo(tokens1Int) + expectedSharesIncrease := exchangeRate1.MulRaw(bondDelegation) + expectedShares := expectedSharesIncrease.Add(bondShares1Int) + + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), bondingWallet.FormattedAddress(), + "staking", "delegate", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", bondDelegation, s.Chain.Config().Denom)) + s.Require().NoError(err) + + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), bondingWallet.FormattedAddress(), + "staking", "validator-bond", providerWallet.ValoperAddress) + s.Require().NoError(err) + + bondShares2Result, err := s.Chain.QueryJSON(s.GetContext(), "validator.validator_bond_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + bondShares2 := bondShares2Result.String() + + bondShares2Int, err := chainsuite.StrToSDKInt(bondShares2) + s.Require().NoError(err) + s.Require().Truef(bondShares2Int.Sub(expectedShares).Abs().LTE(sdkmath.NewInt(1)), "bondShares2: %s, expectedShares: %s", bondShares2, expectedShares) + }) + + s.Run("Delegate via ICA", func() { + preDelegationTokensResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.tokens", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + preDelegationTokens := preDelegationTokensResult.String() + + preDelegationSharesResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + preDelegationShares := preDelegationSharesResult.String() + + preDelegationLiquidSharesResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + preDelegationLiquidShares := preDelegationLiquidSharesResult.String() + + preDelegationTokensInt, err := chainsuite.StrToSDKInt(preDelegationTokens) + s.Require().NoError(err) + preDelegationSharesInt, err := chainsuite.StrToSDKInt(preDelegationShares) + s.Require().NoError(err) + exchangeRate := preDelegationSharesInt.Quo(preDelegationTokensInt) + expectedLiquidIncrease := exchangeRate.MulRaw(delegate) + + delegateHappy := map[string]interface{}{ + "@type": "/cosmos.staking.v1beta1.MsgDelegate", + "delegator_address": s.ICAAddr, + "validator_address": providerWallet.ValoperAddress, + "amount": map[string]interface{}{ + "denom": s.Chain.Config().Denom, + "amount": fmt.Sprint(delegate), + }, + } + delegateHappyJSON, err := json.Marshal(delegateHappy) + s.Require().NoError(err) + jsonPath := "delegate-happy.json" + fullJsonPath := path.Join(s.Stride.Validators[0].HomeDir(), jsonPath) + stdout, _, err := s.Stride.GetNode().ExecBin(s.GetContext(), "tx", "interchain-accounts", "host", "generate-packet-data", string(delegateHappyJSON), "--encoding", "proto3") + s.Require().NoError(err) + s.Require().NoError(s.Stride.Validators[0].WriteFile(s.GetContext(), []byte(stdout), jsonPath)) + ibcChannelStride, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Stride, s.Chain) + s.Require().NoError(err) + + _, err = s.Stride.GetNode().ExecTx(s.GetContext(), strideWallet.Address, + "interchain-accounts", "controller", "send-tx", ibcChannelStride.ConnectionHops[0], fullJsonPath) + s.Require().NoError(err) + + var tokensDelta sdkmath.Int + s.Require().EventuallyWithT(func(c *assert.CollectT) { + postDelegationTokensResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.tokens", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + postDelegationTokens, err := chainsuite.StrToSDKInt(postDelegationTokensResult.String()) + s.Require().NoError(err) + tokensDelta = postDelegationTokens.Sub(preDelegationTokensInt) + assert.Truef(c, tokensDelta.Sub(sdkmath.NewInt(delegate)).Abs().LTE(sdkmath.NewInt(1)), "tokensDelta: %s, delegate: %d", tokensDelta, delegate) + }, 20*time.Second, time.Second) + + postDelegationLiquidSharesResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", providerWallet.ValoperAddress) + s.Require().NoError(err) + postDelegationLiquidShares, err := chainsuite.StrToSDKInt(postDelegationLiquidSharesResult.String()) + s.Require().NoError(err) + preDelegationLiquidSharesInt, err := chainsuite.StrToSDKInt(preDelegationLiquidShares) + s.Require().NoError(err) + liquidSharesDelta := postDelegationLiquidShares.Sub(preDelegationLiquidSharesInt) + s.Require().Truef(liquidSharesDelta.Sub(expectedLiquidIncrease).Abs().LTE(sdkmath.NewInt(1)), "liquidSharesDelta: %s, expectedLiquidIncrease: %d", liquidSharesDelta, expectedLiquidIncrease) + }) +} + +func (s *LSMSuite) TestTokenizeVested() { + const amount = 100_000_000_000 + const vestingPeriod = 100 * time.Second + vestedByTimestamp := time.Now().Add(vestingPeriod).Unix() + vestingAccount, err := s.Chain.BuildWallet(s.GetContext(), fmt.Sprintf("vesting-%d", vestedByTimestamp), "") + s.Require().NoError(err) + validatorWallet := s.Chain.ValidatorWallets[0] + + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), interchaintest.FaucetAccountKeyName, + "vesting", "create-vesting-account", vestingAccount.FormattedAddress(), + fmt.Sprintf("%d%s", amount, s.Chain.Config().Denom), + fmt.Sprintf("%d", vestedByTimestamp)) + s.Require().NoError(err) + + // give the vesting account a little cash for gas fees + err = s.Chain.SendFunds(s.GetContext(), interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Amount: sdkmath.NewInt(5_000), + Denom: s.Chain.Config().Denom, + Address: vestingAccount.FormattedAddress(), + }) + s.Require().NoError(err) + + vestingAmount := int64(amount - 5000) + // delegate the vesting account to the validator + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), vestingAccount.FormattedAddress(), + "staking", "delegate", validatorWallet.ValoperAddress, fmt.Sprintf("%d%s", vestingAmount, s.Chain.Config().Denom)) + s.Require().NoError(err) + + // wait for half the vesting period + time.Sleep(vestingPeriod / 2) + + // try to tokenize full amount. Should fail. + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), vestingAccount.FormattedAddress(), + "staking", "tokenize-share", validatorWallet.ValoperAddress, fmt.Sprintf("%d%s", vestingAmount, s.Chain.Config().Denom), vestingAccount.FormattedAddress(), + "--gas", "auto") + s.Require().Error(err) + + sharesPreTokenizeResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", validatorWallet.ValoperAddress) + s.Require().NoError(err) + sharesPreTokenize := sharesPreTokenizeResult.String() + + // try to tokenize vested amount (i.e. half) should succeed if upgraded + tokenizeAmount := vestingAmount / 2 + _, err = s.Chain.GetNode().ExecTx(s.GetContext(), vestingAccount.FormattedAddress(), + "staking", "tokenize-share", validatorWallet.ValoperAddress, fmt.Sprintf("%d%s", tokenizeAmount, s.Chain.Config().Denom), vestingAccount.FormattedAddress(), + "--gas", "auto") + s.Require().NoError(err) + sharesPostTokenizeResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", validatorWallet.ValoperAddress) + s.Require().NoError(err) + sharesPostTokenize := sharesPostTokenizeResult.String() + s.checkAMinusBEqualsX(sharesPostTokenize, sharesPreTokenize, sdkmath.NewInt(tokenizeAmount).Mul(s.ShareFactor)) +} + +func (s *LSMSuite) setupLSMWallets() { + names := []string{lsmBondingMoniker, lsmLiquid1Moniker, lsmLiquid2Moniker, lsmLiquid3Moniker, lsmOwnerMoniker} + wallets := make(map[string]ibc.Wallet) + eg := new(errgroup.Group) + for _, name := range names { + keyName := "happy_" + name + wallet, err := s.Chain.BuildWallet(s.GetContext(), keyName, "") + s.Require().NoError(err) + wallets[name] = wallet + amount := 500_000_000 + if name == "owner" { + amount = 10_000_000 + } + eg.Go(func() error { + return s.Chain.SendFunds(s.GetContext(), interchaintest.FaucetAccountKeyName, ibc.WalletAmount{ + Amount: sdkmath.NewInt(int64(amount)), + Denom: s.Chain.Config().Denom, + Address: wallet.FormattedAddress(), + }) + }) + } + s.Require().NoError(eg.Wait()) + s.LSMWallets = wallets +} + +func (s *LSMSuite) SetupSuite() { + s.Suite.SetupSuite() + // This is slightly broken while stride is still in the process of being upgraded, so skip if + // going from v21 -> v21 + if semver.Major(s.Env.OldGaiaImageVersion) == s.Env.UpgradeName && s.Env.UpgradeName == "v21" { + s.T().Skip("Skipping LSM when going from v21 -> v21") + } + stride, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, chainsuite.ConsumerConfig{ + ChainName: "stride", + Version: chainsuite.StrideVersion, + Denom: chainsuite.StrideDenom, + TopN: 100, + }) + s.Require().NoError(err) + s.Stride = stride + err = s.Chain.CheckCCV(s.GetContext(), stride, s.Relayer, 1_000_000, 0, 1) + s.Require().NoError(err) + + icaAddr, err := stride.SetupICAAccount(s.GetContext(), s.Chain, s.Relayer, stride.ValidatorWallets[0].Address, 0, 1_000_000_000) + s.Require().NoError(err) + s.ICAAddr = icaAddr + shareFactor, ok := sdkmath.NewIntFromString("1000000000000000000") + s.Require().True(ok) + s.ShareFactor = shareFactor + + s.setupLSMWallets() + s.UpgradeChain() +} + +func TestLSM(t *testing.T) { + s := &LSMSuite{ + Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{ + CreateRelayer: true, + }), + } + suite.Run(t, s) +} diff --git a/tests/interchain/matrix_tool/main.go b/tests/interchain/matrix_tool/main.go index 244a691c396..92172ba3f50 100644 --- a/tests/interchain/matrix_tool/main.go +++ b/tests/interchain/matrix_tool/main.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "math/rand/v2" "os" "os/exec" "path" @@ -105,6 +106,9 @@ func GetTestList() ([]string, error) { retval = append(retval, line) } } + rand.Shuffle(len(retval), func(i, j int) { + retval[i], retval[j] = retval[j], retval[i] + }) return retval, nil } diff --git a/tests/interchain/permissionless_test.go b/tests/interchain/permissionless_test.go index 948177436d2..18ed527cab6 100644 --- a/tests/interchain/permissionless_test.go +++ b/tests/interchain/permissionless_test.go @@ -7,11 +7,13 @@ import ( "path" "path/filepath" "strconv" + "strings" "testing" "time" sdkmath "cosmossdk.io/math" - govtypes "github.cogaia/v21/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" "github.com/cosmos/gaia/v21/tests/interchain/chainsuite" transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" ccvclient "github.com/cosmos/interchain-security/v5/x/ccv/provider/client" @@ -21,6 +23,7 @@ import ( "github.com/strangelove-ventures/interchaintest/v8/ibc" "github.com/strangelove-ventures/interchaintest/v8/testutil" "github.com/stretchr/testify/suite" + "github.com/tidwall/gjson" "github.com/tidwall/sjson" "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" @@ -352,7 +355,7 @@ func (s *PermissionlessConsumersSuite) TestChangePowerShaping() { s.Require().NoError(s.Chain.CheckCCV(s.GetContext(), consumer, s.Relayer, 1_000_000, 0, 1)) - vals, err := consumer.QueryJSON(s.GetContext(), "validators", "comet-validator-set") + vals, err := consumer.QueryJSON(s.GetContext(), "validators", "tendermint-validator-set") s.Require().NoError(err) s.Require().Equal(newValidatorCount, len(vals.Array()), vals) for i := 0; i < newValidatorCount; i++ { @@ -373,10 +376,28 @@ func (s *PermissionlessConsumersSuite) TestConsumerCommissionRate() { _, err = s.Chain.Validators[0].ExecTx(s.GetContext(), s.Chain.ValidatorWallets[0].Moniker, "provider", "opt-in", consumerID) s.Require().NoError(err) } + + images := []ibc.DockerImage{ + { + Repository: "ghcr.io/hyphacoop/ics", + Version: "v4.5.0", + UidGid: "1025:1025", + }, + } + chainID := fmt.Sprintf("%s-test-%d", cfg.ChainName, len(s.Chain.Consumers)+1) + spawnTime := time.Now().Add(chainsuite.ChainSpawnWait) + cfg.Spec = s.Chain.DefaultConsumerChainSpec(s.GetContext(), chainID, cfg, spawnTime, nil) + cfg.Spec.Version = "v4.5.0" + cfg.Spec.Images = images consumer1, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg) s.Require().NoError(err) s.Require().NoError(s.Chain.CheckCCV(s.GetContext(), consumer1, s.Relayer, 1_000_000, 0, 1)) + chainID = fmt.Sprintf("%s-test-%d", cfg.ChainName, len(s.Chain.Consumers)+1) + spawnTime = time.Now().Add(chainsuite.ChainSpawnWait) + cfg.Spec = s.Chain.DefaultConsumerChainSpec(s.GetContext(), chainID, cfg, spawnTime, nil) + cfg.Spec.Version = "v4.5.0" + cfg.Spec.Images = images consumer2, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg) s.Require().NoError(err) s.Require().NoError(s.Chain.CheckCCV(s.GetContext(), consumer2, s.Relayer, 1_000_000, 0, 1)) @@ -448,7 +469,9 @@ func (s *PermissionlessConsumersSuite) TestConsumerCommissionRate() { }) s.Require().NoError(eg.Wait()) - s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), chainsuite.BlocksPerDistribution+3, s.Chain, consumer1, consumer2)) + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), chainsuite.BlocksPerDistribution+2, s.Chain, consumer1, consumer2)) + s.Require().NoError(s.Relayer.ClearTransferChannel(s.GetContext(), s.Chain, consumer1)) + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 2, s.Chain, consumer1, consumer2)) rewardStr, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("total.#(%%\"*%s\")", denom1), "distribution", "rewards", s.Chain.ValidatorWallets[0].Address) s.Require().NoError(err) @@ -491,7 +514,9 @@ func (s *PermissionlessConsumersSuite) TestConsumerCommissionRate() { }) s.Require().NoError(eg.Wait()) - s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), chainsuite.BlocksPerDistribution+3, s.Chain, consumer1, consumer2)) + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), chainsuite.BlocksPerDistribution+2, s.Chain, consumer1, consumer2)) + s.Require().NoError(s.Relayer.ClearTransferChannel(s.GetContext(), s.Chain, consumer1)) + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 2, s.Chain, consumer1, consumer2)) rewardStr, err = s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("total.#(%%\"*%s\")", denom1), "distribution", "rewards", s.Chain.ValidatorWallets[0].Address) s.Require().NoError(err) @@ -570,6 +595,87 @@ func (s *PermissionlessConsumersSuite) TestLaunchWithAllowListThenModify() { s.Require().Equal(4, len(validators.Array())) } +func (s *PermissionlessConsumersSuite) TestRewardsWithChangeover() { + validators := 1 + fullNodes := 0 + genesisChanges := []cosmos.GenesisKV{ + 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.Ucon), + cosmos.NewGenesisKV("app_state.gov.params.min_deposit.0.amount", strconv.Itoa(chainsuite.GovMinDepositAmount)), + } + spec := &interchaintest.ChainSpec{ + Name: "ics-consumer", + ChainName: "ics-consumer", + // Unfortunately, this rc is a bit of a bespoke version; it corresponds to an rc + // in hypha's fork that has a fix for sovereign -> consumer changeovers + Version: "v6.2.0-rc1", + NumValidators: &validators, + NumFullNodes: &fullNodes, + ChainConfig: ibc.ChainConfig{ + Denom: chainsuite.Ucon, + GasPrices: "0.025" + chainsuite.Ucon, + GasAdjustment: 2.0, + Gas: "auto", + ConfigFileOverrides: map[string]any{ + "config/config.toml": chainsuite.DefaultConfigToml(), + }, + ModifyGenesisAmounts: chainsuite.DefaultGenesisAmounts(chainsuite.Ucon), + ModifyGenesis: cosmos.ModifyGenesis(genesisChanges), + Bin: "interchain-security-sd", + Images: []ibc.DockerImage{ + { + Repository: chainsuite.HyphaICSRepo, + Version: "v6.2.0-rc1", + UidGid: chainsuite.ICSUidGuid, + }, + }, + Bech32Prefix: "consumer", + }, + } + consumer, err := s.Chain.AddLinkedChain(s.GetContext(), s.T(), s.Relayer, spec) + s.Require().NoError(err) + + transferCh, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chain, consumer) + s.Require().NoError(err) + rewardDenom := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom("transfer", transferCh.ChannelID, consumer.Config().Denom)).IBCDenom() + + s.UpgradeChain() + + s.changeSovereignToConsumer(consumer, transferCh) + + govAuthority, err := s.Chain.GetGovernanceAddress(s.GetContext()) + s.Require().NoError(err) + rewardDenomsProp := providertypes.MsgChangeRewardDenoms{ + DenomsToAdd: []string{rewardDenom}, + Authority: govAuthority, + } + prop, err := s.Chain.BuildProposal([]cosmos.ProtoMessage{&rewardDenomsProp}, + "add denoms to list of registered reward denoms", + "add denoms to list of registered reward denoms", + "", chainsuite.GovDepositAmount, "", false) + s.Require().NoError(err) + propResult, err := s.Chain.SubmitProposal(s.GetContext(), s.Chain.ValidatorWallets[0].Moniker, prop) + s.Require().NoError(err) + s.Require().NoError(s.Chain.PassProposal(s.GetContext(), propResult.ProposalID)) + + faucetAddrBts, err := consumer.GetAddress(s.GetContext(), interchaintest.FaucetAccountKeyName) + s.Require().NoError(err) + faucetAddr := types.MustBech32ifyAddressBytes(consumer.Config().Bech32Prefix, faucetAddrBts) + _, err = consumer.Validators[0].ExecTx(s.GetContext(), interchaintest.FaucetAccountKeyName, "bank", "send", string(faucetAddr), consumer.ValidatorWallets[0].Address, "1"+consumer.Config().Denom, "--fees", "100000000"+consumer.Config().Denom) + s.Require().NoError(err) + + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), chainsuite.BlocksPerDistribution+2, s.Chain, consumer)) + s.Require().NoError(s.Relayer.ClearTransferChannel(s.GetContext(), s.Chain, consumer)) + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 2, s.Chain, consumer)) + + rewardStr, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("total.#(%%\"*%s\")", rewardDenom), "distribution", "rewards", s.Chain.ValidatorWallets[0].Address) + s.Require().NoError(err) + rewards, err := chainsuite.StrToSDKInt(rewardStr.String()) + s.Require().NoError(err) + s.Require().True(rewards.GT(sdkmath.NewInt(0)), "rewards: %s", rewards.String()) +} + func TestPermissionlessConsumers(t *testing.T) { genesis := chainsuite.DefaultGenesis() genesis = append(genesis, @@ -587,17 +693,123 @@ func TestPermissionlessConsumers(t *testing.T) { }), consumerCfg: chainsuite.ConsumerConfig{ ChainName: "ics-consumer", - Version: "v5.0.0", + Version: "v4.5.0", ShouldCopyProviderKey: allProviderKeysCopied(), Denom: chainsuite.Ucon, TopN: 100, AllowInactiveVals: true, MinStake: 1_000_000, + Spec: &interchaintest.ChainSpec{ + ChainConfig: ibc.ChainConfig{ + Images: []ibc.DockerImage{ + { + Repository: chainsuite.HyphaICSRepo, + Version: "v4.5.0", + UidGid: chainsuite.ICSUidGuid, + }, + }, + }, + }, }, } suite.Run(t, s) } +func (s *PermissionlessConsumersSuite) changeSovereignToConsumer(consumer *chainsuite.Chain, transferCh *ibc.ChannelOutput) { + cfg := s.consumerCfg + cfg.TopN = 0 + currentHeight, err := consumer.Height(s.GetContext()) + s.Require().NoError(err) + initialHeight := uint64(currentHeight) + 60 + cfg.InitialHeight = initialHeight + spawnTime := time.Now().Add(60 * time.Second) + cfg.DistributionTransmissionChannel = transferCh.ChannelID + + err = s.Chain.CreateConsumerPermissionless(s.GetContext(), consumer.Config().ChainID, cfg, spawnTime) + s.Require().NoError(err) + + consumerChains, _, err := s.Chain.GetNode().ExecQuery(s.GetContext(), "provider", "list-consumer-chains") + s.Require().NoError(err) + consumerChain := gjson.GetBytes(consumerChains, fmt.Sprintf("chains.#(chain_id=%q)", consumer.Config().ChainID)) + consumerID := consumerChain.Get("consumer_id").String() + + eg := errgroup.Group{} + for i := range consumer.Validators { + i := i + eg.Go(func() error { + key, _, err := consumer.Validators[i].ExecBin(s.GetContext(), "tendermint", "show-validator") + if err != nil { + return err + } + keyStr := strings.TrimSpace(string(key)) + _, err = s.Chain.Validators[i].ExecTx(s.GetContext(), s.Chain.ValidatorWallets[i].Moniker, "provider", "opt-in", consumerID, keyStr) + return err + }) + } + s.Require().NoError(eg.Wait()) + + s.Require().NoError(err) + time.Sleep(time.Until(spawnTime)) + s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 2, s.Chain)) + + proposal := cosmos.SoftwareUpgradeProposal{ + Deposit: "5000000" + chainsuite.Ucon, + Title: "Changeover", + Name: "sovereign-changeover", + Description: "Changeover", + Height: int64(initialHeight) - 3, + } + upgradeTx, err := consumer.UpgradeProposal(s.GetContext(), interchaintest.FaucetAccountKeyName, proposal) + s.Require().NoError(err) + err = consumer.PassProposal(s.GetContext(), upgradeTx.ProposalID) + s.Require().NoError(err) + + currentHeight, err = consumer.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, consumer) + s.Require().Error(err) + + s.Require().NoError(consumer.StopAllNodes(s.GetContext())) + + genesis, err := consumer.GetNode().GenesisFileContent(s.GetContext()) + s.Require().NoError(err) + + ccvState, _, err := s.Chain.GetNode().ExecQuery(s.GetContext(), "provider", "consumer-genesis", consumerID) + s.Require().NoError(err) + genesis, err = sjson.SetRawBytes(genesis, "app_state.ccvconsumer", ccvState) + s.Require().NoError(err) + + genesis, err = sjson.SetBytes(genesis, "app_state.slashing.params.signed_blocks_window", strconv.Itoa(chainsuite.SlashingWindowConsumer)) + s.Require().NoError(err) + genesis, err = sjson.SetBytes(genesis, "app_state.ccvconsumer.params.reward_denoms", []string{chainsuite.Ucon}) + s.Require().NoError(err) + genesis, err = sjson.SetBytes(genesis, "app_state.ccvconsumer.params.provider_reward_denoms", []string{s.Chain.Config().Denom}) + s.Require().NoError(err) + genesis, err = sjson.SetBytes(genesis, "app_state.ccvconsumer.params.blocks_per_distribution_transmission", chainsuite.BlocksPerDistribution) + s.Require().NoError(err) + + for _, val := range consumer.Validators { + val := val + eg.Go(func() error { + if err := val.OverwriteGenesisFile(s.GetContext(), []byte(genesis)); err != nil { + return err + } + return val.WriteFile(s.GetContext(), []byte(genesis), ".sovereign/config/genesis.json") + }) + } + s.Require().NoError(eg.Wait()) + + consumer.ChangeBinary(s.GetContext(), "interchain-security-cdd") + s.Require().NoError(consumer.StartAllNodes(s.GetContext())) + s.Require().NoError(s.Relayer.ConnectProviderConsumer(s.GetContext(), s.Chain, consumer)) + s.Require().NoError(s.Relayer.StopRelayer(s.GetContext(), chainsuite.GetRelayerExecReporter(s.GetContext()))) + s.Require().NoError(s.Relayer.StartRelayer(s.GetContext(), chainsuite.GetRelayerExecReporter(s.GetContext()))) + s.Require().NoError(s.Chain.CheckCCV(s.GetContext(), consumer, s.Relayer, 1_000_000, 0, 1)) +} + func (s *PermissionlessConsumersSuite) submitChangeRewardDenoms(consumer *chainsuite.Chain) (string, string) { consumerCh, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chain, consumer) s.Require().NoError(err) diff --git a/tests/interchain/pfm_test.go b/tests/interchain/pfm_test.go new file mode 100644 index 00000000000..867102bb0b0 --- /dev/null +++ b/tests/interchain/pfm_test.go @@ -0,0 +1,133 @@ +package interchain_test + +import ( + "encoding/json" + "testing" + + "github.com/cosmos/gaia/v21/tests/interchain/chainsuite" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type PFMSuite struct { + *chainsuite.Suite + Chains []*chainsuite.Chain +} + +func (s *PFMSuite) SetupSuite() { + s.Suite.SetupSuite() + chainB, err := s.Chain.AddLinkedChain(s.GetContext(), s.T(), s.Relayer, chainsuite.DefaultChainSpec(s.Env)) + s.Require().NoError(err) + chainC, err := chainB.AddLinkedChain(s.GetContext(), s.T(), s.Relayer, chainsuite.DefaultChainSpec(s.Env)) + s.Require().NoError(err) + chainD, err := chainC.AddLinkedChain(s.GetContext(), s.T(), s.Relayer, chainsuite.DefaultChainSpec(s.Env)) + s.Require().NoError(err) + + s.Chains = []*chainsuite.Chain{s.Chain, chainB, chainC, chainD} +} + +func (s *PFMSuite) TestPFMHappyPath() { + var forwardChannels []*ibc.ChannelOutput + targetDenomAD := s.Chains[0].Config().Denom + for i := 0; i < len(s.Chains)-1; i++ { + transferCh, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chains[i], s.Chains[i+1]) + s.Require().NoError(err) + forwardChannels = append(forwardChannels, transferCh) + targetDenomAD = transfertypes.GetPrefixedDenom(transferCh.PortID, transferCh.Counterparty.ChannelID, targetDenomAD) + } + targetDenomAD = transfertypes.ParseDenomTrace(targetDenomAD).IBCDenom() + + // backwardChannels[2] = chain3 -> chain2, backwardChannels[1] = chain2 -> chain1, backwardChannels[0] = chain1 -> chain0 + backwardChannels := make([]*ibc.ChannelOutput, len(forwardChannels)) + targetDenomDA := s.Chains[3].Config().Denom + for i := len(s.Chains) - 1; i > 0; i-- { + transferCh, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chains[i], s.Chains[i-1]) + s.Require().NoError(err) + backwardChannels[i-1] = transferCh + targetDenomDA = transfertypes.GetPrefixedDenom(transferCh.PortID, transferCh.Counterparty.ChannelID, targetDenomDA) + } + targetDenomDA = transfertypes.ParseDenomTrace(targetDenomDA).IBCDenom() + + dWallet1 := s.Chains[3].ValidatorWallets[0] + + aWallet1 := s.Chains[0].ValidatorWallets[0] + + dStartBalance, err := s.Chains[3].GetBalance(s.GetContext(), dWallet1.Address, targetDenomAD) + s.Require().NoError(err) + + timeout := "10m" + memo := map[string]interface{}{ + "forward": map[string]interface{}{ + "receiver": "pfm", + "port": "transfer", + "channel": forwardChannels[1].ChannelID, + "timeout": timeout, + "next": map[string]interface{}{ + "forward": map[string]interface{}{ + "receiver": dWallet1.Address, + "port": "transfer", + "channel": forwardChannels[2].ChannelID, + "timeout": timeout, + }, + }, + }, + } + memoBytes, err := json.Marshal(memo) + s.Require().NoError(err) + _, err = s.Chains[0].GetNode().ExecTx(s.GetContext(), aWallet1.Address, + "ibc-transfer", "transfer", "transfer", forwardChannels[0].ChannelID, "pfm", "1000000"+s.Chains[0].Config().Denom, + "--memo", string(memoBytes)) + s.Require().NoError(err) + + s.Require().EventuallyWithT(func(c *assert.CollectT) { + dEndBalance, err := s.Chains[3].GetBalance(s.GetContext(), dWallet1.Address, targetDenomAD) + assert.NoError(c, err) + assert.Truef(c, dEndBalance.Sub(dStartBalance).IsPositive(), "expected %d - %d > 0 (it was %d) in %s", + dEndBalance, dStartBalance, dEndBalance.Sub(dStartBalance), targetDenomAD) + }, 30*chainsuite.CommitTimeout, chainsuite.CommitTimeout, "chain D balance has not increased") + + aStartBalance, err := s.Chains[0].GetBalance(s.GetContext(), aWallet1.Address, targetDenomDA) + s.Require().NoError(err) + + memo = map[string]interface{}{ + "forward": map[string]interface{}{ + "receiver": "pfm", + "port": "transfer", + "channel": backwardChannels[1].ChannelID, + "timeout": timeout, + "next": map[string]interface{}{ + "forward": map[string]interface{}{ + "receiver": aWallet1.Address, + "port": "transfer", + "channel": backwardChannels[0].ChannelID, + "timeout": timeout, + }, + }, + }, + } + memoBytes, err = json.Marshal(memo) + s.Require().NoError(err) + _, err = s.Chains[3].GetNode().ExecTx(s.GetContext(), dWallet1.Address, + "ibc-transfer", "transfer", "transfer", backwardChannels[2].ChannelID, "pfm", "1000000"+s.Chains[3].Config().Denom, + "--memo", string(memoBytes)) + s.Require().NoError(err) + + s.Require().EventuallyWithT(func(c *assert.CollectT) { + aEndBalance, err := s.Chains[0].GetBalance(s.GetContext(), aWallet1.Address, targetDenomDA) + assert.NoError(c, err) + assert.Truef(c, aEndBalance.Sub(aStartBalance).IsPositive(), "expected %d - %d > 0 (it was %d) in %s", + aEndBalance, aStartBalance, aEndBalance.Sub(aStartBalance), targetDenomDA) + }, 30*chainsuite.CommitTimeout, chainsuite.CommitTimeout, "chain A balance has not increased") + +} + +func TestPFM(t *testing.T) { + s := &PFMSuite{ + Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{ + UpgradeOnSetup: true, + CreateRelayer: true, + })} + suite.Run(t, s) +} diff --git a/tests/interchain/unbonding_test.go b/tests/interchain/unbonding_test.go index 86c7656b333..105d5dbbb57 100644 --- a/tests/interchain/unbonding_test.go +++ b/tests/interchain/unbonding_test.go @@ -12,6 +12,7 @@ import ( "github.com/strangelove-ventures/interchaintest/v8/ibc" "github.com/strangelove-ventures/interchaintest/v8/testutil" "github.com/stretchr/testify/suite" + "golang.org/x/mod/semver" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -31,10 +32,21 @@ func (s *UnbondingSuite) SetupSuite() { s.Suite.SetupSuite() cfg := chainsuite.ConsumerConfig{ ChainName: "ics-consumer", - Version: "v5.0.0", + Version: selectConsumerVersion("v6.0.0", "v6.2.1"), ShouldCopyProviderKey: allProviderKeysCopied(), Denom: chainsuite.Ucon, TopN: 100, + Spec: &interchaintest.ChainSpec{ + ChainConfig: ibc.ChainConfig{ + Images: []ibc.DockerImage{ + { + Repository: chainsuite.HyphaICSRepo, + Version: selectConsumerVersion("v6.0.0", "v6.2.1"), + UidGid: chainsuite.ICSUidGuid, + }, + }, + }, + }, } consumer, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg) s.Require().NoError(err) @@ -107,9 +119,14 @@ func (s *UnbondingSuite) TestCanLaunchAfterInitTimeout() { func TestUnbonding(t *testing.T) { genesis := chainsuite.DefaultGenesis() + env := chainsuite.GetEnvironment() + if semver.Compare(env.OldGaiaImageVersion, "v20.0.0") < 0 { + genesis = append(genesis, + cosmos.NewGenesisKV("app_state.provider.params.vsc_timeout_period", vscTimeoutPeriod.String()), + cosmos.NewGenesisKV("app_state.provider.params.init_timeout_period", initTimeoutPeriod.String()), + ) + } genesis = append(genesis, - cosmos.NewGenesisKV("app_state.provider.params.vsc_timeout_period", vscTimeoutPeriod.String()), - cosmos.NewGenesisKV("app_state.provider.params.init_timeout_period", initTimeoutPeriod.String()), cosmos.NewGenesisKV("app_state.staking.params.unbonding_time", unbondingTime.String()), ) s := &UnbondingSuite{