diff --git a/.github/workflows/local-interchain.yaml b/.github/workflows/local-interchain.yaml index 52e498a22..f674b3368 100644 --- a/.github/workflows/local-interchain.yaml +++ b/.github/workflows/local-interchain.yaml @@ -37,6 +37,40 @@ jobs: name: local-ic path: ~/go/bin/local-ic + bash-e2e: + name: bash + needs: build + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./local-interchain + strategy: + fail-fast: false + + steps: + - name: checkout chain + uses: actions/checkout@v4 + + - name: Download Tarball Artifact + uses: actions/download-artifact@v3 + with: + name: local-ic + path: /tmp + + - name: Make local-ic executable + run: chmod +x /tmp/local-ic + + - name: Start background ibc local-interchain + run: /tmp/local-ic start juno_ibc --api-port 8080 & + + - name: Run Bash Script + run: | + cd bash + bash ./test.bash + + - name: Cleanup + run: killall local-ic && exit 0 + rust-e2e: name: rust needs: build diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 067fc1aae..3a191012b 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -25,4 +25,4 @@ jobs: # run tests - name: run unit tests # -short flag purposefully omitted because there are some longer unit tests - run: go test -race -timeout 10m -failfast -p 2 $(go list ./... | grep -v /cmd | grep -v /examples) + run: go test -race -timeout 30m -failfast -p 2 $(go list ./... | grep -v /cmd | grep -v /examples) diff --git a/chain/cosmos/chain_node.go b/chain/cosmos/chain_node.go index 7487bb1de..90d0e0c33 100644 --- a/chain/cosmos/chain_node.go +++ b/chain/cosmos/chain_node.go @@ -147,7 +147,7 @@ func (tn *ChainNode) NewClient(addr string) error { tn.Client = rpcClient - grpcConn, err := grpc.Dial( + grpcConn, err := grpc.NewClient( tn.hostGRPCPort, grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { diff --git a/chain/cosmos/module_slashing.go b/chain/cosmos/module_slashing.go index 4fab32583..3f5c98439 100644 --- a/chain/cosmos/module_slashing.go +++ b/chain/cosmos/module_slashing.go @@ -18,19 +18,28 @@ func (tn *ChainNode) SlashingUnJail(ctx context.Context, keyName string) error { func (c *CosmosChain) SlashingQueryParams(ctx context.Context) (*slashingtypes.Params, error) { res, err := slashingtypes.NewQueryClient(c.GetNode().GrpcConn). Params(ctx, &slashingtypes.QueryParamsRequest{}) - return &res.Params, err + if err != nil { + return nil, err + } + return &res.Params, nil } // SlashingSigningInfo returns signing info for a validator func (c *CosmosChain) SlashingQuerySigningInfo(ctx context.Context, consAddress string) (*slashingtypes.ValidatorSigningInfo, error) { res, err := slashingtypes.NewQueryClient(c.GetNode().GrpcConn). SigningInfo(ctx, &slashingtypes.QuerySigningInfoRequest{ConsAddress: consAddress}) - return &res.ValSigningInfo, err + if err != nil { + return nil, err + } + return &res.ValSigningInfo, nil } // SlashingSigningInfos returns all signing infos func (c *CosmosChain) SlashingQuerySigningInfos(ctx context.Context) ([]slashingtypes.ValidatorSigningInfo, error) { res, err := slashingtypes.NewQueryClient(c.GetNode().GrpcConn). SigningInfos(ctx, &slashingtypes.QuerySigningInfosRequest{}) - return res.Info, err + if err != nil { + return nil, err + } + return res.Info, nil } diff --git a/chain/cosmos/module_staking.go b/chain/cosmos/module_staking.go index 60727300a..f37ef4f17 100644 --- a/chain/cosmos/module_staking.go +++ b/chain/cosmos/module_staking.go @@ -84,97 +84,136 @@ func (tn *ChainNode) StakingCreateValidatorFile( func (c *CosmosChain) StakingQueryDelegation(ctx context.Context, valAddr string, delegator string) (*stakingtypes.DelegationResponse, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). Delegation(ctx, &stakingtypes.QueryDelegationRequest{DelegatorAddr: delegator, ValidatorAddr: valAddr}) - return res.DelegationResponse, err + if err != nil { + return nil, err + } + return res.DelegationResponse, nil } // StakingQueryDelegations returns all delegations for a delegator. func (c *CosmosChain) StakingQueryDelegations(ctx context.Context, delegator string) ([]stakingtypes.DelegationResponse, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). DelegatorDelegations(ctx, &stakingtypes.QueryDelegatorDelegationsRequest{DelegatorAddr: delegator, Pagination: nil}) - return res.DelegationResponses, err + if err != nil { + return nil, err + } + return res.DelegationResponses, nil } // StakingQueryDelegationsTo returns all delegations to a validator. func (c *CosmosChain) StakingQueryDelegationsTo(ctx context.Context, validator string) ([]*stakingtypes.DelegationResponse, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). ValidatorDelegations(ctx, &stakingtypes.QueryValidatorDelegationsRequest{ValidatorAddr: validator}) + if err != nil { + return nil, err + } var delegations []*stakingtypes.DelegationResponse for _, d := range res.DelegationResponses { delegations = append(delegations, &d) } - return delegations, err + return delegations, nil } // StakingQueryDelegatorValidator returns a validator for a delegator. func (c *CosmosChain) StakingQueryDelegatorValidator(ctx context.Context, delegator string, validator string) (*stakingtypes.Validator, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). DelegatorValidator(ctx, &stakingtypes.QueryDelegatorValidatorRequest{DelegatorAddr: delegator, ValidatorAddr: validator}) - return &res.Validator, err + if err != nil { + return nil, err + } + return &res.Validator, nil } // StakingQueryDelegatorValidators returns all validators for a delegator. func (c *CosmosChain) StakingQueryDelegatorValidators(ctx context.Context, delegator string) ([]stakingtypes.Validator, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). DelegatorValidators(ctx, &stakingtypes.QueryDelegatorValidatorsRequest{DelegatorAddr: delegator}) - return res.Validators, err + if err != nil { + return nil, err + } + return res.Validators, nil } // StakingQueryHistoricalInfo returns the historical info at the given height. func (c *CosmosChain) StakingQueryHistoricalInfo(ctx context.Context, height int64) (*stakingtypes.HistoricalInfo, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). HistoricalInfo(ctx, &stakingtypes.QueryHistoricalInfoRequest{Height: height}) - return res.Hist, err + if err != nil { + return nil, err + } + return res.Hist, nil } // StakingQueryParams returns the staking parameters. func (c *CosmosChain) StakingQueryParams(ctx context.Context) (*stakingtypes.Params, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). Params(ctx, &stakingtypes.QueryParamsRequest{}) - return &res.Params, err + if err != nil { + return nil, err + } + return &res.Params, nil } // StakingQueryPool returns the current staking pool values. func (c *CosmosChain) StakingQueryPool(ctx context.Context) (*stakingtypes.Pool, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). Pool(ctx, &stakingtypes.QueryPoolRequest{}) - return &res.Pool, err + if err != nil { + return nil, err + } + return &res.Pool, nil } // StakingQueryRedelegation returns a redelegation. func (c *CosmosChain) StakingQueryRedelegation(ctx context.Context, delegator string, srcValAddr string, dstValAddr string) ([]stakingtypes.RedelegationResponse, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). Redelegations(ctx, &stakingtypes.QueryRedelegationsRequest{DelegatorAddr: delegator, SrcValidatorAddr: srcValAddr, DstValidatorAddr: dstValAddr}) - return res.RedelegationResponses, err + if err != nil { + return nil, err + } + return res.RedelegationResponses, nil } // StakingQueryUnbondingDelegation returns an unbonding delegation. func (c *CosmosChain) StakingQueryUnbondingDelegation(ctx context.Context, delegator string, validator string) (*stakingtypes.UnbondingDelegation, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). UnbondingDelegation(ctx, &stakingtypes.QueryUnbondingDelegationRequest{DelegatorAddr: delegator, ValidatorAddr: validator}) - return &res.Unbond, err + if err != nil { + return nil, err + } + return &res.Unbond, nil } // StakingQueryUnbondingDelegations returns all unbonding delegations for a delegator. func (c *CosmosChain) StakingQueryUnbondingDelegations(ctx context.Context, delegator string) ([]stakingtypes.UnbondingDelegation, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). DelegatorUnbondingDelegations(ctx, &stakingtypes.QueryDelegatorUnbondingDelegationsRequest{DelegatorAddr: delegator}) - return res.UnbondingResponses, err + if err != nil { + return nil, err + } + return res.UnbondingResponses, nil } // StakingQueryUnbondingDelegationsFrom returns all unbonding delegations from a validator. func (c *CosmosChain) StakingQueryUnbondingDelegationsFrom(ctx context.Context, validator string) ([]stakingtypes.UnbondingDelegation, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). ValidatorUnbondingDelegations(ctx, &stakingtypes.QueryValidatorUnbondingDelegationsRequest{ValidatorAddr: validator}) - return res.UnbondingResponses, err + if err != nil { + return nil, err + } + return res.UnbondingResponses, nil } // StakingQueryValidator returns a validator. func (c *CosmosChain) StakingQueryValidator(ctx context.Context, validator string) (*stakingtypes.Validator, error) { res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn). Validator(ctx, &stakingtypes.QueryValidatorRequest{ValidatorAddr: validator}) - return &res.Validator, err + if err != nil { + return nil, err + } + return &res.Validator, nil } // StakingQueryValidators returns all validators. @@ -182,5 +221,8 @@ func (c *CosmosChain) StakingQueryValidators(ctx context.Context, status string) res, err := stakingtypes.NewQueryClient(c.GetNode().GrpcConn).Validators(ctx, &stakingtypes.QueryValidatorsRequest{ Status: status, }) - return res.Validators, err + if err != nil { + return nil, err + } + return res.Validators, nil } diff --git a/chain/penumbra/penumbra_client_node.go b/chain/penumbra/penumbra_client_node.go index 848912a67..8e210166d 100644 --- a/chain/penumbra/penumbra_client_node.go +++ b/chain/penumbra/penumbra_client_node.go @@ -582,7 +582,7 @@ func (p *PenumbraClientNode) StartContainer(ctx context.Context) error { p.hostGRPCPort = hostPorts[0] - p.GRPCConn, err = grpc.Dial(p.hostGRPCPort, grpc.WithTransportCredentials(insecure.NewCredentials())) + p.GRPCConn, err = grpc.NewClient(p.hostGRPCPort, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return err } diff --git a/chain/thorchain/module_bank.go b/chain/thorchain/module_bank.go index 25919410b..ef540809b 100644 --- a/chain/thorchain/module_bank.go +++ b/chain/thorchain/module_bank.go @@ -25,46 +25,70 @@ func (c *Thorchain) GetBalance(ctx context.Context, address string, denom string // BankGetBalance is an alias for GetBalance func (c *Thorchain) BankQueryBalance(ctx context.Context, address string, denom string) (sdkmath.Int, error) { res, err := banktypes.NewQueryClient(c.GetNode().GrpcConn).Balance(ctx, &banktypes.QueryBalanceRequest{Address: address, Denom: denom}) - return res.Balance.Amount, err + if err != nil { + return sdkmath.ZeroInt(), err + } + return res.Balance.Amount, nil } // AllBalances fetches an account address's balance for all denoms it holds func (c *Thorchain) BankQueryAllBalances(ctx context.Context, address string) (types.Coins, error) { res, err := banktypes.NewQueryClient(c.GetNode().GrpcConn).AllBalances(ctx, &banktypes.QueryAllBalancesRequest{Address: address}) - return res.GetBalances(), err + if err != nil { + return nil, err + } + return res.GetBalances(), nil } // BankDenomMetadata fetches the metadata of a specific coin denomination func (c *Thorchain) BankQueryDenomMetadata(ctx context.Context, denom string) (*banktypes.Metadata, error) { res, err := banktypes.NewQueryClient(c.GetNode().GrpcConn).DenomMetadata(ctx, &banktypes.QueryDenomMetadataRequest{Denom: denom}) - return &res.Metadata, err + if err != nil { + return nil, err + } + return &res.Metadata, nil } func (c *Thorchain) BankQueryDenomMetadataByQueryString(ctx context.Context, denom string) (*banktypes.Metadata, error) { res, err := banktypes.NewQueryClient(c.GetNode().GrpcConn).DenomMetadataByQueryString(ctx, &banktypes.QueryDenomMetadataByQueryStringRequest{Denom: denom}) - return &res.Metadata, err + if err != nil { + return nil, err + } + return &res.Metadata, nil } func (c *Thorchain) BankQueryDenomOwners(ctx context.Context, denom string) ([]*banktypes.DenomOwner, error) { res, err := banktypes.NewQueryClient(c.GetNode().GrpcConn).DenomOwners(ctx, &banktypes.QueryDenomOwnersRequest{Denom: denom}) - return res.DenomOwners, err + if err != nil { + return nil, err + } + return res.DenomOwners, nil } func (c *Thorchain) BankQueryDenomsMetadata(ctx context.Context) ([]banktypes.Metadata, error) { res, err := banktypes.NewQueryClient(c.GetNode().GrpcConn).DenomsMetadata(ctx, &banktypes.QueryDenomsMetadataRequest{}) - return res.Metadatas, err + if err != nil { + return nil, err + } + return res.Metadatas, nil } func (c *Thorchain) BankQueryParams(ctx context.Context) (*banktypes.Params, error) { res, err := banktypes.NewQueryClient(c.GetNode().GrpcConn).Params(ctx, &banktypes.QueryParamsRequest{}) - return &res.Params, err + if err != nil { + return nil, err + } + return &res.Params, nil } func (c *Thorchain) BankQuerySendEnabled(ctx context.Context, denoms []string) ([]*banktypes.SendEnabled, error) { res, err := banktypes.NewQueryClient(c.GetNode().GrpcConn).SendEnabled(ctx, &banktypes.QuerySendEnabledRequest{ Denoms: denoms, }) - return res.SendEnabled, err + if err != nil { + return nil, err + } + return res.SendEnabled, nil } func (c *Thorchain) BankQuerySpendableBalance(ctx context.Context, address, denom string) (*types.Coin, error) { @@ -72,21 +96,32 @@ func (c *Thorchain) BankQuerySpendableBalance(ctx context.Context, address, deno Address: address, Denom: denom, }) - return res.Balance, err + if err != nil { + return nil, err + } + return res.Balance, nil } func (c *Thorchain) BankQuerySpendableBalances(ctx context.Context, address string) (*types.Coins, error) { res, err := banktypes.NewQueryClient(c.GetNode().GrpcConn).SpendableBalances(ctx, &banktypes.QuerySpendableBalancesRequest{Address: address}) - return &res.Balances, err + if err != nil { + return nil, err + } + return &res.Balances, nil } func (c *Thorchain) BankQueryTotalSupply(ctx context.Context) (*types.Coins, error) { res, err := banktypes.NewQueryClient(c.GetNode().GrpcConn).TotalSupply(ctx, &banktypes.QueryTotalSupplyRequest{}) - return &res.Supply, err + if err != nil { + return nil, err + } + return &res.Supply, nil } func (c *Thorchain) BankQueryTotalSupplyOf(ctx context.Context, address string) (*types.Coin, error) { res, err := banktypes.NewQueryClient(c.GetNode().GrpcConn).SupplyOf(ctx, &banktypes.QuerySupplyOfRequest{Denom: address}) - - return &res.Amount, err + if err != nil { + return nil, err + } + return &res.Amount, nil } diff --git a/chain/thorchain/thornode.go b/chain/thorchain/thornode.go index cfd2fdc26..b7c8d9309 100644 --- a/chain/thorchain/thornode.go +++ b/chain/thorchain/thornode.go @@ -143,7 +143,7 @@ func (tn *ChainNode) NewClient(addr string) error { tn.Client = rpcClient - grpcConn, err := grpc.Dial( + grpcConn, err := grpc.NewClient( tn.hostGRPCPort, grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { diff --git a/examples/thorchain/helper.go b/examples/thorchain/helper.go index 565a0c975..d279944d2 100644 --- a/examples/thorchain/helper.go +++ b/examples/thorchain/helper.go @@ -5,7 +5,6 @@ import ( "fmt" "regexp" "strings" - "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v8/ibc" ) diff --git a/examples/thorchain/setup.go b/examples/thorchain/setup.go index f52b6da95..bda4ad2b6 100644 --- a/examples/thorchain/setup.go +++ b/examples/thorchain/setup.go @@ -16,7 +16,6 @@ import ( "github.com/docker/docker/client" ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/strangelove-ventures/interchaintest/v8" "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" @@ -158,11 +157,24 @@ func StartThorchain(t *testing.T, ctx context.Context, client *client.Client, ne return thorchain } -func SetupContracts(t *testing.T, ctx context.Context, exoChain *ExoChain) string { - if exoChain.chain.Config().Bin == "geth" { - return SetupGethContracts(t, ctx, exoChain) - } - return SetupAnvilContracts(t, ctx, exoChain) +func SetupContracts(ctx context.Context, ethExoChain *ExoChain, bscExoChain *ExoChain) (ethContractAddr, bscContractAddr string, err error) { + eg, egCtx := errgroup.WithContext(ctx) + eg.Go(func() error { + var err error + if ethExoChain.chain.Config().Bin == "geth" { + ethContractAddr, err = SetupGethContracts(egCtx, ethExoChain) + } else { + ethContractAddr, err = SetupAnvilContracts(egCtx, ethExoChain) + } + return err + }) + eg.Go(func() error { + var err error + bscContractAddr, err = SetupGethContracts(egCtx, bscExoChain) + return err + }) + + return ethContractAddr, bscContractAddr, eg.Wait() } //go:embed contracts/eth-router-abi.json @@ -177,7 +189,7 @@ var routerAbi []byte //go:embed contracts/router-bytecode.txt var routerByteCode []byte -func SetupGethContracts(t *testing.T, ctx context.Context, exoChain *ExoChain) string { +func SetupGethContracts(ctx context.Context, exoChain *ExoChain) (string, error) { abi := routerAbi byteCode := routerByteCode if exoChain.chain.Config().Name == "ETH" { @@ -190,66 +202,79 @@ func SetupGethContracts(t *testing.T, ctx context.Context, exoChain *ExoChain) s ethUserInitialAmount := ethereum.ETHER.MulRaw(100) ethUser, err := interchaintest.GetAndFundTestUserWithMnemonic(ctx, "user", strings.Repeat("dog ", 23)+"fossil", ethUserInitialAmount, ethChain) - require.NoError(t, err) - - faucetAddrBz, err := ethChain.GetAddress(ctx, "faucet") - require.NoError(t, err) - - balance, err := ethChain.GetBalance(ctx, hexutil.Encode(faucetAddrBz), "") - require.NoError(t, err) - fmt.Println("Faucet balance:", balance) - - balance, err = ethChain.GetBalance(ctx, ethUser.FormattedAddress(), "") - require.NoError(t, err) - fmt.Println("Dog balance:", balance) + if err != nil { + return "", err + } ethRouterContractAddress, err := ethChain.DeployContract(ctx, ethUser.KeyName(), abi, byteCode) - require.NoError(t, err) - fmt.Println("Router contract addr", ethRouterContractAddress) - require.NotEmpty(t, ethRouterContractAddress) - require.True(t, ethcommon.IsHexAddress(ethRouterContractAddress)) + if err != nil { + return "", err + } + if ethRouterContractAddress == "" { + return "", fmt.Errorf("router contract address for (%s) chain is empty", ethChain.Config().Name) + } + if !ethcommon.IsHexAddress(ethRouterContractAddress) { + return "", fmt.Errorf("router contract address for (%s) chain is not a hex address", ethChain.Config().Name) + } - return ethRouterContractAddress + return ethRouterContractAddress, nil } -func SetupAnvilContracts(t *testing.T, ctx context.Context, exoChain *ExoChain) string { +func SetupAnvilContracts(ctx context.Context, exoChain *ExoChain) (string, error) { ethChain := exoChain.chain.(*foundry.AnvilChain) ethUserInitialAmount := ethereum.ETHER.MulRaw(2) ethUser, err := interchaintest.GetAndFundTestUserWithMnemonic(ctx, "user", strings.Repeat("dog ", 23)+"fossil", ethUserInitialAmount, ethChain) - require.NoError(t, err) + if err != nil { + return "", err + } stdout, _, err := ethChain.ForgeScript(ctx, ethUser.KeyName(), foundry.ForgeScriptOpts{ ContractRootDir: "contracts", SolidityContract: "script/Token.s.sol", RawOptions: []string{"--sender", ethUser.FormattedAddress(), "--json"}, }) - require.NoError(t, err) + if err != nil { + return "", err + } tokenContractAddress, err := GetEthAddressFromStdout(string(stdout)) - require.NoError(t, err) - require.NotEmpty(t, tokenContractAddress) - require.True(t, ethcommon.IsHexAddress(tokenContractAddress)) + if err != nil { + return "", err + } + if tokenContractAddress == "" { + return "", fmt.Errorf("token contract address for (%s) chain is empty", ethChain.Config().Name) + } + if !ethcommon.IsHexAddress(tokenContractAddress) { + return "", fmt.Errorf("token contract address for (%s) chain is not a hex address", ethChain.Config().Name) + } stdout, _, err = ethChain.ForgeScript(ctx, ethUser.KeyName(), foundry.ForgeScriptOpts{ ContractRootDir: "contracts", SolidityContract: "script/Router.s.sol", RawOptions: []string{"--sender", ethUser.FormattedAddress(), "--json"}, }) - require.NoError(t, err) + if err != nil { + return "", err + } ethRouterContractAddress, err := GetEthAddressFromStdout(string(stdout)) - require.NoError(t, err) - require.NotEmpty(t, ethRouterContractAddress) - require.True(t, ethcommon.IsHexAddress(ethRouterContractAddress)) + if err != nil { + return "", err + } + if ethRouterContractAddress == "" { + return "", fmt.Errorf("router contract address for (%s) chain is empty", ethChain.Config().Name) + } + if !ethcommon.IsHexAddress(ethRouterContractAddress) { + return "", fmt.Errorf("router contract address for (%s) chain is not a hex address", ethChain.Config().Name) + } - return ethRouterContractAddress + return ethRouterContractAddress, nil } func SetupGaia(t *testing.T, ctx context.Context, exoChain *ExoChain) *errgroup.Group { gaia := exoChain.chain.(*cosmos.CosmosChain) - eg, egCtx := errgroup.WithContext(ctx) eg.Go(func() error { for _, genWallet := range exoChain.genWallets { @@ -258,7 +283,6 @@ func SetupGaia(t *testing.T, ctx context.Context, exoChain *ExoChain) *errgroup. return err } } - amount := ibc.WalletAmount{ Denom: gaia.Config().Denom, Amount: sdkmath.NewInt(1_000_000), diff --git a/examples/thorchain/thorchain_test.go b/examples/thorchain/thorchain_test.go index fce8c01a5..60b47caa0 100644 --- a/examples/thorchain/thorchain_test.go +++ b/examples/thorchain/thorchain_test.go @@ -27,8 +27,8 @@ func TestThorchainSim(t *testing.T) { // Start non-thorchain chains exoChains := StartExoChains(t, ctx, client, network) gaiaEg := SetupGaia(t, ctx, exoChains["GAIA"]) - bscRouterContractAddress := SetupContracts(t, ctx, exoChains["BSC"]) - ethRouterContractAddress := SetupContracts(t, ctx, exoChains["ETH"]) + ethRouterContractAddress, bscRouterContractAddress, err := SetupContracts(ctx, exoChains["ETH"], exoChains["BSC"]) + require.NoError(t, err) // Start thorchain thorchain := StartThorchain(t, ctx, client, network, exoChains, ethRouterContractAddress, bscRouterContractAddress) @@ -71,7 +71,7 @@ func TestThorchainSim(t *testing.T) { // -------------------------------------------------------- // Arb // -------------------------------------------------------- - _, err := features.Arb(t, ctx, thorchain, exoChains.GetChains()...) + _, err = features.Arb(t, ctx, thorchain, exoChains.GetChains()...) require.NoError(t, err) // -------------------------------------------------------- diff --git a/ibc/types.go b/ibc/types.go index a992988e2..d99699774 100644 --- a/ibc/types.go +++ b/ibc/types.go @@ -439,9 +439,9 @@ type PathUpdateOptions struct { } type ICSConfig struct { - ProviderVerOverride string `yaml:"provider,omitempty" json:"provider,omitempty"` - ConsumerVerOverride string `yaml:"consumer,omitempty" json:"consumer,omitempty"` - ConsumerCopyProviderKey func(int) bool + ProviderVerOverride string `yaml:"provider,omitempty" json:"provider,omitempty"` + ConsumerVerOverride string `yaml:"consumer,omitempty" json:"consumer,omitempty"` + ConsumerCopyProviderKey func(int) bool `yaml:"-" json:"-"` } // GenesisConfig is used to start a chain from a pre-defined genesis state. diff --git a/interchain_test.go b/interchain_test.go index 1ae422a13..9d7062759 100644 --- a/interchain_test.go +++ b/interchain_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "testing" + "time" "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/codec" @@ -255,6 +256,96 @@ func TestCosmosChain_BroadcastTx_HermesRelayer(t *testing.T) { broadcastTxCosmosChainTest(t, ibc.Hermes) } +func TestInterchain_ConcurrentRelayerOps(t *testing.T) { + type relayerTest struct { + relayer ibc.RelayerImplementation + name string + } + + const ( + denom = "uatom" + chains = 4 + ) + + relayers := []relayerTest{ + { + relayer: ibc.CosmosRly, + name: "Cosmos Relayer", + }, + { + relayer: ibc.Hermes, + name: "Hermes", + }, + } + + numFullNodes := 0 + numValidators := 1 + + for _, rly := range relayers { + rly := rly + t.Run(rly.name, func(t *testing.T) { + client, network := interchaintest.DockerSetup(t) + f, err := interchaintest.CreateLogFile(fmt.Sprintf("%d.json", time.Now().Unix())) + require.NoError(t, err) + // Reporter/logs + rep := testreporter.NewReporter(f) + eRep := rep.RelayerExecReporter(t) + ctx := context.Background() + + chainSpecs := make([]*interchaintest.ChainSpec, chains) + for i := 0; i < chains; i++ { + chainSpecs[i] = &interchaintest.ChainSpec{ + Name: "gaia", + ChainName: fmt.Sprintf("g%d", i+1), + Version: "v7.0.1", + NumValidators: &numValidators, + NumFullNodes: &numFullNodes, + ChainConfig: ibc.ChainConfig{ + GasPrices: "0" + denom, + Denom: denom, + }, + } + } + r := interchaintest.NewBuiltinRelayerFactory(rly.relayer, zaptest.NewLogger(t)).Build( + t, client, network, + ) + + cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), chainSpecs) + chains, err := cf.Chains(t.Name()) + require.NoError(t, err) + ic := interchaintest.NewInterchain() + for _, chain := range chains { + require.NoError(t, err) + ic.AddChain(chain) + } + ic.AddRelayer(r, "relayer") + for i, chainI := range chains { + for j := i + 1; j < len(chains); j++ { + ic.AddLink(interchaintest.InterchainLink{ + Chain1: chainI, + Chain2: chains[j], + Relayer: r, + Path: getIBCPath(chainI, chains[j]), + }) + } + } + err = ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + }) + require.NoError(t, err) + t.Cleanup(func() { + ic.Close() + }) + }) + } +} + +func getIBCPath(chainA, chainB ibc.Chain) string { + return chainA.Config().ChainID + "-" + chainB.Config().ChainID +} + func broadcastTxCosmosChainTest(t *testing.T, relayerImpl ibc.RelayerImplementation) { if testing.Short() { t.Skip("skipping in short mode") diff --git a/local-interchain/bash/source.bash b/local-interchain/bash/source.bash new file mode 100755 index 000000000..e0ad4dca6 --- /dev/null +++ b/local-interchain/bash/source.bash @@ -0,0 +1,152 @@ +# IMPORT ME WITH: source <(curl -s https://raw.githubusercontent.com/strangelove-ventures/interchaintest/main/local-interchain/bash/source.bash) + +# exitIfEmpty "$someKey" someKey +function ICT_exitIfEmpty() { + if [ -z "$1" ]; then + echo "Exiting because ${2} is empty" + exit 1 + fi +} + +# === BASE === + +# ICT_MAKE_REQUEST http://127.0.0.1:8080 localjuno-1 "q" "bank total" +ICT_MAKE_REQUEST() { + local API=$1 CHAIN_ID=$2 ACTION=$3 + shift 3 # get the 4th argument and up as the command + local COMMAND="$*" + + DATA=`printf '{"chain_id":"%s","action":"%s","cmd":"MYCOMMAND"}' $CHAIN_ID $ACTION` + DATA=`echo $DATA | sed "s/MYCOMMAND/$COMMAND/g"` + + curl "$API" -ss --no-progress-meter --header "Content-Type: application/json" -X POST -d "$DATA" +} + +# ICT_QUERY "http://localhost:8080" "localjuno-1" "bank balances juno10r39fueph9fq7a6lgswu4zdsg8t3gxlq670lt0" +ICT_QUERY() { + local API=$1 CHAIN_ID=$2 CMD=$3 # can be multiple words + ICT_MAKE_REQUEST "$API" $CHAIN_ID "q" "$CMD" +} + +# ICT_BIN "http://localhost:8080" "localjuno-1" "decode" +ICT_BIN() { + local API=$1 CHAIN_ID=$2 CMD=$3 # can be multiple words + ICT_MAKE_REQUEST "$API" $CHAIN_ID "bin" "$CMD" +} + +# ICT_SH_EXEC "http://localhost:8080" "localjuno-1" "ls -l" +# NOTE: if using a /, make sure to escape it with \ +ICT_SH_EXEC() { + local API=$1 CHAIN_ID=$2 CMD=$3 # can be multiple words + ICT_MAKE_REQUEST "$API" $CHAIN_ID "exec" "$CMD" +} + +# === RELAYER === + +# ICT_RELAYER_STOP http://127.0.0.1 "localjuno-1" +ICT_RELAYER_STOP() { + local API=$1 CHAIN_ID=$2 + + # TODO: how does this function? + ICT_MAKE_REQUEST $API $CHAIN_ID "stop-relayer" "" +} + +# ICT_RELAYER_START http://127.0.0.1 "localjuno-1" "demo-path2 --max-tx-size 10" +ICT_RELAYER_START() { + local API=$1 CHAIN_ID=$2 CMD=$3 + + ICT_MAKE_REQUEST $API $CHAIN_ID "start-relayer" "$CMD" +} + +# RELAYER_EXEC http://127.0.0.1:8080 "localjuno-1" "rly paths list" +ICT_RELAYER_EXEC() { + local API=$1 CHAIN_ID=$2 + shift 2 # get the 3rd argument and up as the command + local CMD="$*" + + ICT_MAKE_REQUEST $API $CHAIN_ID "relayer-exec" "$CMD" +} + +# RELAYER_CHANNELS http://127.0.0.1:8080 "localjuno-1" +ICT_RELAYER_CHANNELS() { + local API=$1 CHAIN_ID=$2 + + ICT_MAKE_REQUEST $API $CHAIN_ID "get_channels" "" +} + +# === COSMWASM === + +# ICT_WASM_DUMP_CONTRACT_STATE "http://localhost:8080" "localjuno-1" "cosmos1contractaddress" "100" +ICT_WASM_DUMP_CONTRACT_STATE() { + local API=$1 CHAIN_ID=$2 CONTRACT=$3 HEIGHT=$4 + + ICT_MAKE_REQUEST $API $CHAIN_ID "recover-key" "contract=$CONTRACT;height=$HEIGHT" +} + +# ICT_WASM_STORE_FILE "http://localhost:8080" "localjuno-1" "/host/absolute/path.wasm" "keyName" +# returns the code_id of the uploaded contract +ICT_WASM_STORE_FILE() { + local API=$1 CHAIN_ID=$2 FILE=$3 KEYNAME=$4 + + DATA=`printf '{"chain_id":"%s","file_path":"%s","key_name":"%s"}' $CHAIN_ID $FILE $KEYNAME` + curl "$API/upload" --header "Content-Type: application/json" --header "Upload-Type: cosmwasm" -X POST -d "$DATA" +} + +# === OTHER === + +# ICT_POLL_FOR_START "http://localhost:8080" 50 +ICT_POLL_FOR_START() { + local API=$1 ATTEMPTS_MAX=$2 + + curl --head -X GET --retry $ATTEMPTS_MAX --retry-connrefused --retry-delay 3 $API +} + +# ICT_KILL_ALL "http://localhost:8080" "localjuno-1" +# (Kills all running, keeps local-ic process. `killall local-ic` to kill that as well) +ICT_KILL_ALL() { + local API=$1 CHAIN_ID=$2 + ICT_MAKE_REQUEST $API $CHAIN_ID "kill-all" "" +} + +# ICT_GET_PEER "http://localhost:8080" "localjuno-1" +ICT_GET_PEER() { + local API=$1 CHAIN_ID=$2 + + if [[ $API != */info ]]; then + API="$API/info" + fi + + curl -G -d "chain_id=$CHAIN_ID" -d "request=peer" $API +} + +# ICT_FAUCET_REQUEST "http://localhost:8080" "localjuno-1" "1000000000ujuno" "juno1qk7zqy3k2v3jx2zq2z2zq2zq2zq2zq2zq2zq" +ICT_FAUCET_REQUEST() { + local API=$1 CHAIN_ID=$2 AMOUNT=$3 ADDRESS=$4 + ICT_MAKE_REQUEST $API $CHAIN_ID "faucet" "amount=$AMOUNT;address=$ADDRESS" +} + +# ICT_ADD_FULL_NODE http://127.0.0.1:8080 "localjuno-1" "1" +ICT_ADD_FULL_NODE() { + local API=$1 CHAIN_ID=$2 AMOUNT=$3 + + ICT_MAKE_REQUEST $API $CHAIN_ID "add-full-nodes" "amount=$AMOUNT" +} + +# ICT_RECOVER_KEY "http://localhost:8080" "localjuno-1" "mykey" "my mnemonic string here" +ICT_RECOVER_KEY() { + local API=$1 CHAIN_ID=$2 KEYNAME=$3 + shift 3 # get the 4th argument and up as the command + local MNEMONIC="$*" + + ICT_MAKE_REQUEST $API $CHAIN_ID "recover-key" "keyname=$KEYNAME;mnemonic=$MNEMONIC" +} + +# ICT_STORE_FILE "http://localhost:8080" "localjuno-1" "/host/absolute/path" +# Uploads any arbitrary host file to the chain node. +ICT_STORE_FILE() { + local API=$1 CHAIN_ID=$2 FILE=$3 + + DATA=`printf '{"chain_id":"%s","file_path":"%s"}' $CHAIN_ID $FILE` + curl "$API/upload" --header "Content-Type: application/json" -X POST -d "$DATA" +} + diff --git a/local-interchain/bash/test.bash b/local-interchain/bash/test.bash new file mode 100755 index 000000000..9fc37ed3a --- /dev/null +++ b/local-interchain/bash/test.bash @@ -0,0 +1,88 @@ +#!/bin/bash +# local-ic start juno_ibc +# +# bash local-interchain/bash/test.bash + +# exits if any command is non 0 status +set -e + +thisDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# EXTERNAL: source <(curl -s https://raw.githubusercontent.com/strangelove-ventures/interchaintest/main/local-interchain/bash/source.bash) +source "$thisDir/source.bash" +API_ADDR="http://localhost:8080" + +# === BEGIN TESTS === + +ICT_POLL_FOR_START $API_ADDR 50 + +# Set standard interaction defaults +ICT_BIN "$API_ADDR" "localjuno-1" "config keyring-backend test" +ICT_BIN "$API_ADDR" "localjuno-1" "config output json" + +# Get total bank supply +BANK_TOTAL=`ICT_QUERY $API_ADDR "localjuno-1" "bank total"` && echo "BANK_TOTAL: $BANK_TOTAL" +ICT_exitIfEmpty "$BANK_TOTAL" "BANK_TOTAL" +echo $BANK_TOTAL | jq -r '.supply' + +# Get total bank supply another way (directly) +BANK_TOTAL=`ICT_MAKE_REQUEST $API_ADDR "localjuno-1" "q" "bank total"` && echo "BANK_TOTAL: $BANK_TOTAL" +ICT_exitIfEmpty "$BANK_TOTAL" "BANK_TOTAL" +echo $BANK_TOTAL | jq -r '.supply' + +# faucet to user +FAUCET_RES=`ICT_FAUCET_REQUEST "$API_ADDR" "localjuno-1" "7" "juno10r39fueph9fq7a6lgswu4zdsg8t3gxlq670lt0"` && echo "FAUCET_RES: $FAUCET_RES" +FAUCET_CONFIRM=`ICT_QUERY $API_ADDR "localjuno-1" "bank balances juno10r39fueph9fq7a6lgswu4zdsg8t3gxlq670lt0"` && echo "FAUCET_CONFIRM: $FAUCET_CONFIRM" +ICT_exitIfEmpty "$FAUCET_CONFIRM" "FAUCET_CONFIRM" + +if [ $(echo $FAUCET_CONFIRM | jq -r '.balances[0].amount') -lt 7 ]; then + echo "FAUCET_CONFIRM is less than 7" + exit 1 +fi + +# CosmWasm - Upload source file to chain & store +parent_dir=$(dirname $thisDir) # local-interchain folder +contract_source="$parent_dir/contracts/cw_ibc_example.wasm" +CODE_ID_JSON=`ICT_WASM_STORE_FILE $API_ADDR "localjuno-1" "$contract_source" "acc0"` && echo "CODE_ID_JSON: $CODE_ID_JSON" +CODE_ID=`echo $CODE_ID_JSON | jq -r '.code_id'` && echo "CODE_ID: $CODE_ID" +ICT_exitIfEmpty "$CODE_ID" "CODE_ID" + +# Upload random file +FILE_RESP=`ICT_STORE_FILE $API_ADDR "localjuno-1" "$thisDir/test.bash"` && echo "FILE_RESP: $FILE_RESP" +FILE_LOCATION=`echo $FILE_RESP | jq -r '.location'` && echo "FILE_LOCATION: $FILE_LOCATION" +ICT_exitIfEmpty "$FILE_LOCATION" "FILE_LOCATION" + +# Verify file contents are there +FILE_LOCATION_ESC=$(echo $FILE_LOCATION | sed 's/\//\\\//g') +MISC_BASH_CMD=`ICT_SH_EXEC "$API_ADDR" "localjuno-1" "cat $FILE_LOCATION_ESC"` && echo "MISC_BASH_CMD: $MISC_BASH_CMD" +ICT_exitIfEmpty "$MISC_BASH_CMD" "MISC_BASH_CMD" + +PEER=`ICT_GET_PEER $API_ADDR "localjuno-1"` && echo "PEER: $PEER" +ICT_exitIfEmpty "$PEER" "PEER" + +# RELAYER +CHANNELS=`ICT_RELAYER_CHANNELS $API_ADDR "localjuno-1"` && echo "CHANNELS: $CHANNELS" +ICT_exitIfEmpty "$CHANNELS" "CHANNELS" + +ICT_RELAYER_EXEC $API_ADDR "localjuno-1" "rly paths list" +ICT_RELAYER_EXEC $API_ADDR "localjuno-1" "rly chains list" +RLY_BALANCE=`ICT_RELAYER_EXEC $API_ADDR "localjuno-1" "rly q balance localjuno-1 --output=json"` && echo "RLY_BALANCE: $RLY_BALANCE" +ICT_exitIfEmpty "$RLY_BALANCE" "RLY_BALANCE" +echo $RLY_BALANCE | jq -r '.balance' + + +# Recover a key and validate +COSMOS_KEY_STATUS=`ICT_RECOVER_KEY $API_ADDR "localjuno-1" "mynewkey" "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"` && echo "COSMOS_KEY_STATUS: $COSMOS_KEY_STATUS" + +COSMOS_KEY_ADDRESS=`ICT_BIN "$API_ADDR" "localjuno-1" "keys show mynewkey -a"` && echo "COSMOS_KEY_ADDRESS: $COSMOS_KEY_ADDRESS" +ICT_exitIfEmpty "$COSMOS_KEY_ADDRESS" "COSMOS_KEY_ADDRESS" + +FULL_NODE_ADDED=`ICT_ADD_FULL_NODE $API_ADDR "localjuno-1" "1"` +ICT_exitIfEmpty "$FULL_NODE_ADDED" "FULL_NODE_ADDED" + +# Stop the relayer +ICT_RELAYER_STOP $API_ADDR "localjuno-1" + +# Kills all containers, not the local-ic process. Use `killall local-ic` to kill that as well +ICT_KILL_ALL $API_ADDR "localjuno-1" + +exit 0 \ No newline at end of file diff --git a/local-interchain/interchain/handlers/actions.go b/local-interchain/interchain/handlers/actions.go index 85998837f..f21b1f8a9 100644 --- a/local-interchain/interchain/handlers/actions.go +++ b/local-interchain/interchain/handlers/actions.go @@ -151,21 +151,29 @@ func (a *actions) PostActions(w http.ResponseWriter, r *http.Request) { // Relayer Actions if the above is not used. if len(stdout) == 0 && len(stderr) == 0 && err == nil { - if err := a.relayerCheck(w, r); err != nil { - return - } - switch action { case "stop-relayer", "stop_relayer", "stopRelayer": + if err := a.relayerCheck(w, r); err != nil { + return + } + err = a.relayer.StopRelayer(a.ctx, a.eRep) case "start-relayer", "start_relayer", "startRelayer": + if err := a.relayerCheck(w, r); err != nil { + return + } + paths := strings.FieldsFunc(ah.Cmd, func(c rune) bool { return c == ',' || c == ' ' }) err = a.relayer.StartRelayer(a.ctx, a.eRep, paths...) case "relayer", "relayer-exec", "relayer_exec", "relayerExec": + if err := a.relayerCheck(w, r); err != nil { + return + } + if !strings.Contains(ah.Cmd, "--home") { // does this ever change for any other relayer? cmd = append(cmd, "--home", "/home/relayer") @@ -177,6 +185,10 @@ func (a *actions) PostActions(w http.ResponseWriter, r *http.Request) { err = res.Err case "get_channels", "get-channels", "getChannels": + if err := a.relayerCheck(w, r); err != nil { + return + } + res, err := a.relayer.GetChannels(a.ctx, a.eRep, chainId) if err != nil { util.WriteError(w, err) diff --git a/local-interchain/interchain/handlers/chain_registry.go b/local-interchain/interchain/handlers/chain_registry.go new file mode 100644 index 000000000..b98ac522e --- /dev/null +++ b/local-interchain/interchain/handlers/chain_registry.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "net/http" + "os" +) + +// If the chain_registry.json file is found within the current running directory, show it as an enpoint. +// Used in: spawn + +type chainRegistry struct { + DataJSON []byte `json:"data_json"` +} + +// NewChainRegistry creates a new chainRegistry with the JSON from the file at location. +func NewChainRegistry(loc string) *chainRegistry { + dataJSON, err := os.ReadFile(loc) + if err != nil { + panic(err) + } + + return &chainRegistry{ + DataJSON: dataJSON, + } +} + +func (cr chainRegistry) GetChainRegistry(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if _, err := w.Write(cr.DataJSON); err != nil { + http.Error(w, "failed to write response", http.StatusInternalServerError) + } + +} diff --git a/local-interchain/interchain/router/router.go b/local-interchain/interchain/router/router.go index 15d03b86c..c93bc3b87 100644 --- a/local-interchain/interchain/router/router.go +++ b/local-interchain/interchain/router/router.go @@ -3,7 +3,10 @@ package router import ( "context" "encoding/json" + "log" "net/http" + "os" + "path/filepath" "github.com/gorilla/mux" ictypes "github.com/strangelove-ventures/interchaintest/local-interchain/interchain/types" @@ -36,6 +39,27 @@ func NewRouter( infoH := handlers.NewInfo(config, installDir, ctx, ic, cosmosChains, vals, relayer, eRep) r.HandleFunc("/info", infoH.GetInfo).Methods(http.MethodGet) + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + chainRegistryFile := filepath.Join(wd, "chain_registry.json") + if _, err := os.Stat(chainRegistryFile); err == nil { + crH := handlers.NewChainRegistry(chainRegistryFile) + r.HandleFunc("/chain_registry", crH.GetChainRegistry).Methods(http.MethodGet) + } else { + log.Printf("chain_registry.json not found in %s, not exposing endpoint.", wd) + } + + chainRegistryAssetsFile := filepath.Join(wd, "chain_registry_assets.json") + if _, err := os.Stat(chainRegistryAssetsFile); err == nil { + crH := handlers.NewChainRegistry(chainRegistryAssetsFile) + r.HandleFunc("/chain_registry_assets", crH.GetChainRegistry).Methods(http.MethodGet) + } else { + log.Printf("chain_registry_assets.json not found in %s, not exposing endpoint.", wd) + } + actionsH := handlers.NewActions(ctx, ic, cosmosChains, vals, relayer, eRep, authKey) r.HandleFunc("/", actionsH.PostActions).Methods(http.MethodPost) diff --git a/local-interchain/interchain/start.go b/local-interchain/interchain/start.go index dc1368cec..607bc6f58 100644 --- a/local-interchain/interchain/start.go +++ b/local-interchain/interchain/start.go @@ -13,6 +13,7 @@ import ( "sync" "syscall" + "github.com/gorilla/handlers" "github.com/strangelove-ventures/interchaintest/local-interchain/interchain/router" "github.com/strangelove-ventures/interchaintest/local-interchain/interchain/types" "github.com/strangelove-ventures/interchaintest/v8" @@ -244,8 +245,25 @@ func StartChain(installDir, chainCfgFile string, ac *types.AppStartConfig) { Port: fmt.Sprintf("%d", ac.Port), } - server := fmt.Sprintf("%s:%s", config.Server.Host, config.Server.Port) - if err := http.ListenAndServe(server, r); err != nil { + if config.Server.Host == "" { + config.Server.Host = "127.0.0.1" + } + if config.Server.Port == "" { + config.Server.Port = "8080" + } + + serverAddr := fmt.Sprintf("%s:%s", config.Server.Host, config.Server.Port) + + // Where ORIGIN_ALLOWED is like `scheme://dns[:port]`, or `*` (insecure) + corsHandler := handlers.CORS( + handlers.AllowedOrigins([]string{"*"}), + handlers.AllowedHeaders([]string{"*"}), + handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS", "DELETE"}), + handlers.AllowCredentials(), + handlers.ExposedHeaders([]string{"*"}), + ) + + if err := http.ListenAndServe(serverAddr, corsHandler(r)); err != nil { log.Default().Println(err) } }() diff --git a/relayer/hermes/hermes_relayer.go b/relayer/hermes/hermes_relayer.go index 06bae49b8..47f7f4577 100644 --- a/relayer/hermes/hermes_relayer.go +++ b/relayer/hermes/hermes_relayer.go @@ -6,6 +6,7 @@ import ( "fmt" "regexp" "strings" + "sync" "time" "github.com/docker/docker/client" @@ -35,8 +36,12 @@ var ( // Relayer is the ibc.Relayer implementation for hermes. type Relayer struct { *relayer.DockerRelayer + + // lock protects the relayer's state + lock sync.RWMutex paths map[string]*pathConfiguration chainConfigs []ChainConfig + chainLocks map[string]*sync.Mutex } // ChainConfig holds all values required to write an entry in the "chains" section in the hermes config file. @@ -72,6 +77,7 @@ func NewHermesRelayer(log *zap.Logger, testName string, cli *client.Client, netw return &Relayer{ DockerRelayer: dr, + chainLocks: map[string]*sync.Mutex{}, } } @@ -87,13 +93,21 @@ func (r *Relayer) AddChainConfiguration(ctx context.Context, rep ibc.RelayerExec return fmt.Errorf("failed to write hermes config: %w", err) } - return r.validateConfig(ctx, rep) + if err := r.validateConfig(ctx, rep); err != nil { + return err + } + r.lock.Lock() + defer r.lock.Unlock() + r.chainLocks[chainConfig.ChainID] = &sync.Mutex{} + return nil } // LinkPath performs the operations that happen when a path is linked. This includes creating clients, creating connections // and establishing a channel. This happens across multiple operations rather than a single link path cli command. func (r *Relayer) LinkPath(ctx context.Context, rep ibc.RelayerExecReporter, pathName string, channelOpts ibc.CreateChannelOptions, clientOpts ibc.CreateClientOptions) error { + r.lock.RLock() _, ok := r.paths[pathName] + r.lock.RUnlock() if !ok { return fmt.Errorf("path %s not found", pathName) } @@ -114,7 +128,12 @@ func (r *Relayer) LinkPath(ctx context.Context, rep ibc.RelayerExecReporter, pat } func (r *Relayer) CreateChannel(ctx context.Context, rep ibc.RelayerExecReporter, pathName string, opts ibc.CreateChannelOptions) error { - pathConfig := r.paths[pathName] + pathConfig, unlock, err := r.getAndLockPath(pathName) + if err != nil { + return err + } + defer unlock() + cmd := []string{hermes, "--json", "create", "channel", "--order", opts.Order.String(), "--a-chain", pathConfig.chainA.chainID, "--a-port", opts.SourcePortName, "--b-port", opts.DestPortName, "--a-connection", pathConfig.chainA.connectionID} if opts.Version != "" { cmd = append(cmd, "--channel-version", opts.Version) @@ -129,7 +148,12 @@ func (r *Relayer) CreateChannel(ctx context.Context, rep ibc.RelayerExecReporter } func (r *Relayer) CreateConnections(ctx context.Context, rep ibc.RelayerExecReporter, pathName string) error { - pathConfig := r.paths[pathName] + pathConfig, unlock, err := r.getAndLockPath(pathName) + if err != nil { + return err + } + defer unlock() + cmd := []string{hermes, "--json", "create", "connection", "--a-chain", pathConfig.chainA.chainID, "--a-client", pathConfig.chainA.clientID, "--b-client", pathConfig.chainB.clientID} res := r.Exec(ctx, rep, cmd, nil) @@ -147,10 +171,12 @@ func (r *Relayer) CreateConnections(ctx context.Context, rep ibc.RelayerExecRepo } func (r *Relayer) UpdateClients(ctx context.Context, rep ibc.RelayerExecReporter, pathName string) error { - pathConfig, ok := r.paths[pathName] - if !ok { - return fmt.Errorf("path %s not found", pathName) + pathConfig, unlock, err := r.getAndLockPath(pathName) + if err != nil { + return err } + defer unlock() + updateChainACmd := []string{hermes, "--json", "update", "client", "--host-chain", pathConfig.chainA.chainID, "--client", pathConfig.chainA.clientID} res := r.Exec(ctx, rep, updateChainACmd, nil) if res.Err != nil { @@ -164,7 +190,12 @@ func (r *Relayer) UpdateClients(ctx context.Context, rep ibc.RelayerExecReporter // Note: in the go relayer this can be done with a single command using the path reference, // however in Hermes this needs to be done as two separate commands. func (r *Relayer) CreateClients(ctx context.Context, rep ibc.RelayerExecReporter, pathName string, opts ibc.CreateClientOptions) error { - pathConfig := r.paths[pathName] + pathConfig, unlock, err := r.getAndLockPath(pathName) + if err != nil { + return err + } + defer unlock() + chainACreateClientCmd := []string{hermes, "--json", "create", "client", "--host-chain", pathConfig.chainA.chainID, "--reference-chain", pathConfig.chainB.chainID} if opts.TrustingPeriod != "" { chainACreateClientCmd = append(chainACreateClientCmd, "--trusting-period", opts.TrustingPeriod) @@ -205,7 +236,11 @@ func (r *Relayer) CreateClients(ctx context.Context, rep ibc.RelayerExecReporter } func (r *Relayer) CreateClient(ctx context.Context, rep ibc.RelayerExecReporter, srcChainID, dstChainID, pathName string, opts ibc.CreateClientOptions) error { - pathConfig := r.paths[pathName] + pathConfig, unlock, err := r.getAndLockPath(pathName) + if err != nil { + return err + } + defer unlock() createClientCmd := []string{hermes, "--json", "create", "client", "--host-chain", srcChainID, "--reference-chain", dstChainID} if opts.TrustingPeriod != "" { @@ -262,6 +297,8 @@ func (r *Relayer) RestoreKey(ctx context.Context, rep ibc.RelayerExecReporter, c } func (r *Relayer) UpdatePath(ctx context.Context, rep ibc.RelayerExecReporter, pathName string, opts ibc.PathUpdateOptions) error { + r.lock.Lock() + defer r.lock.Unlock() // the concept of paths doesn't exist in hermes, but update our in-memory paths so we can use them elsewhere path, ok := r.paths[pathName] if !ok { @@ -289,6 +326,7 @@ func (r *Relayer) UpdatePath(ctx context.Context, rep ibc.RelayerExecReporter, p } func (r *Relayer) Flush(ctx context.Context, rep ibc.RelayerExecReporter, pathName string, channelID string) error { + r.lock.RLock() path := r.paths[pathName] channels, err := r.GetChannels(ctx, rep, path.chainA.chainID) if err != nil { @@ -304,6 +342,7 @@ func (r *Relayer) Flush(ctx context.Context, rep ibc.RelayerExecReporter, pathNa if portID == "" { return fmt.Errorf("channel %s not found on chain %s", channelID, path.chainA.chainID) } + r.lock.RUnlock() cmd := []string{hermes, "clear", "packets", "--chain", path.chainA.chainID, "--channel", channelID, "--port", portID} res := r.Exec(ctx, rep, cmd, nil) return res.Err @@ -312,6 +351,8 @@ func (r *Relayer) Flush(ctx context.Context, rep ibc.RelayerExecReporter, pathNa // GeneratePath establishes an in memory path representation. The concept does not exist in hermes, so it is handled // at the interchain test level. func (r *Relayer) GeneratePath(ctx context.Context, rep ibc.RelayerExecReporter, srcChainID, dstChainID, pathName string) error { + r.lock.Lock() + defer r.lock.Unlock() if r.paths == nil { r.paths = map[string]*pathConfiguration{} } @@ -330,6 +371,8 @@ func (r *Relayer) GeneratePath(ctx context.Context, rep ibc.RelayerExecReporter, // rather than multiple config files, we need to maintain a list of chain configs each time they are added to write the // full correct file update calling Relayer.AddChainConfiguration. func (r *Relayer) configContent(cfg ibc.ChainConfig, keyName, rpcAddr, grpcAddr string) ([]byte, error) { + r.lock.Lock() + defer r.lock.Unlock() r.chainConfigs = append(r.chainConfigs, ChainConfig{ cfg: cfg, keyName: keyName, @@ -367,6 +410,25 @@ func extractJsonResult(stdout []byte) []byte { return []byte(jsonOutput) } +func (r *Relayer) getAndLockPath(pathName string) (*pathConfiguration, func(), error) { + // we don't get an RLock here because we could deadlock while trying to get the chain locks + r.lock.Lock() + path, ok := r.paths[pathName] + defer r.lock.Unlock() + if !ok { + return nil, nil, fmt.Errorf("path %s not found", pathName) + } + chainALock := r.chainLocks[path.chainA.chainID] + chainBLock := r.chainLocks[path.chainB.chainID] + chainALock.Lock() + chainBLock.Lock() + unlock := func() { + chainALock.Unlock() + chainBLock.Unlock() + } + return path, unlock, nil +} + // GetClientIdFromStdout extracts the client ID from stdout. func GetClientIdFromStdout(stdout []byte) (string, error) { var clientCreationResult ClientCreationResponse