diff --git a/Makefile b/Makefile index d789c684a..149103fd1 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,17 @@ GAIA_VERSION := v7.0.1 AKASH_VERSION := v0.16.3 OSMOSIS_VERSION := v8.0.0 WASMD_VERSION := v0.25.0 +DOCKER := $(shell which docker) GOPATH := $(shell go env GOPATH) GOBIN := $(GOPATH)/bin +containerProtoVer=v0.2 +containerProtoImage=tendermintdev/sdk-proto-gen:$(containerProtoVer) +containerProtoGen=cosmos-sdk-proto-gen-$(containerProtoVer) +containerProtoGenSwagger=cosmos-sdk-proto-gen-swagger-$(containerProtoVer) +containerProtoFmt=cosmos-sdk-proto-fmt-$(containerProtoVer) + all: lint install ############################################################################### @@ -82,7 +89,7 @@ ibctest-multiple: cd ibctest && go test -race -v -run TestRelayerMultiplePathsSingleProcess . ibctest-scenario: ## Scenario tests are suitable for simple networks of 1 validator and no full nodes. They test specific functionality. - cd ibctest && go test -race -v -run TestScenario . + cd ibctest && go test -race -v -run TestScenario ./... coverage: @echo "viewing test coverage..." @@ -154,3 +161,9 @@ release: -w /go/src/$(PACKAGE_NAME) \ goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ release --rm-dist + +proto-gen: + @echo "Generating Protobuf files" + @if docker ps -a --format '{{.Names}}' | grep -Eq "^${containerProtoGen}$$"; then docker start -a $(containerProtoGen); else docker run --name $(containerProtoGen) -v $(CURDIR):/workspace --workdir /workspace $(containerProtoImage) \ + sh ./scripts/protocgen.sh; fi + @go mod tidy \ No newline at end of file diff --git a/go.mod b/go.mod index 3938c8813..0496202f0 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,14 @@ go 1.19 require ( github.com/avast/retry-go/v4 v4.3.1 + github.com/cosmos/cosmos-proto v1.0.0-alpha7 github.com/cosmos/cosmos-sdk v0.46.6 github.com/cosmos/ibc-go/v5 v5.1.0 github.com/gogo/protobuf v1.3.3 github.com/google/go-cmp v0.5.9 github.com/google/go-github/v43 v43.0.0 + github.com/gorilla/mux v1.8.0 + github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/jsternberg/zap-logfmt v1.3.0 github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b github.com/prometheus/client_golang v1.12.2 @@ -52,7 +55,6 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/confio/ics23/go v0.7.0 // indirect github.com/cosmos/btcutil v1.0.4 // indirect - github.com/cosmos/cosmos-proto v1.0.0-alpha7 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gorocksdb v1.2.0 // indirect github.com/cosmos/iavl v0.19.4 // indirect @@ -85,7 +87,6 @@ require ( github.com/googleapis/go-type-adapters v1.0.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect - github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect @@ -93,7 +94,6 @@ require ( github.com/hashicorp/go-getter v1.6.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/go-uuid v1.0.1 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect github.com/hashicorp/hcl v1.0.0 // indirect diff --git a/go.sum b/go.sum index 44984691f..e49aa7036 100644 --- a/go.sum +++ b/go.sum @@ -372,6 +372,7 @@ github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= @@ -396,9 +397,8 @@ github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjh github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= diff --git a/ibctest/stride/codecs.go b/ibctest/stride/codecs.go new file mode 100644 index 000000000..081d382cf --- /dev/null +++ b/ibctest/stride/codecs.go @@ -0,0 +1,15 @@ +package stride + +import ( + simappparams "github.com/cosmos/cosmos-sdk/simapp/params" + rlystride "github.com/cosmos/relayer/v2/relayer/chains/cosmos/stride" + "github.com/strangelove-ventures/ibctest/v5/chain/cosmos" +) + +func Encoding() *simappparams.EncodingConfig { + cfg := cosmos.DefaultEncoding() + + rlystride.RegisterInterfaces(cfg.InterfaceRegistry) + + return &cfg +} diff --git a/ibctest/stride/setup_test.go b/ibctest/stride/setup_test.go new file mode 100644 index 000000000..dd72eed0a --- /dev/null +++ b/ibctest/stride/setup_test.go @@ -0,0 +1,261 @@ +package stride_test + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + chantypes "github.com/cosmos/ibc-go/v5/modules/core/04-channel/types" + rlystride "github.com/cosmos/relayer/v2/relayer/chains/cosmos/stride" + "github.com/icza/dyno" + "github.com/strangelove-ventures/ibctest/v5/chain/cosmos" + "github.com/strangelove-ventures/ibctest/v5/ibc" + "github.com/strangelove-ventures/ibctest/v5/test" +) + +const ( + StrideAdminAccount = "admin" + StrideAdminMnemonic = "tone cause tribe this switch near host damage idle fragile antique tail soda alien depth write wool they rapid unfold body scan pledge soft" +) + +const ( + DayEpochIndex = 1 + DayEpochLen = "100s" + StrideEpochIndex = 2 + StrideEpochLen = "40s" + IntervalLen = 1 + VotingPeriod = "30s" + MaxDepositPeriod = "30s" + UnbondingTime = "200s" + TrustingPeriod = "199s" +) + +var AllowMessages = []string{ + "/cosmos.bank.v1beta1.MsgSend", + "/cosmos.bank.v1beta1.MsgMultiSend", + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", + "/cosmos.staking.v1beta1.MsgTokenizeShares", + "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", + "/ibc.applications.transfer.v1.MsgTransfer", +} + +type HostZoneAccount struct { + Address string `json:"address"` + // Delegations [] `json:"delegations"` + Target string `json:"target"` +} + +type HostZoneValidator struct { + Address string `json:"address"` + CommissionRate string `json:"commissionRate"` + DelegationAmt string `json:"delegationAmt"` + InternalExchangeRate string `json:"internalExchangeRate"` + Name string `json:"name"` + Status string `json:"status"` + Weight string `json:"weight"` +} + +type HostZoneWrapper struct { + HostZone HostZone `json:"HostZone"` +} + +type HostZone struct { + HostDenom string `json:"HostDenom"` + IBCDenom string `json:"IBCDenom"` + LastRedemptionRate string `json:"LastRedemptionRate"` + RedemptionRate string `json:"RedemptionRate"` + Address string `json:"address"` + Bech32prefix string `json:"bech32pref ix"` + ChainID string `json:"chainId"` + ConnectionID string `json:"connectionId"` + DelegationAccount HostZoneAccount `json:"delegationAccount"` + FeeAccount HostZoneAccount `json:"feeAccount"` + RedemptionAccount HostZoneAccount `json:"redemptionAccount"` + WithdrawalAccount HostZoneAccount `json:"withdrawalAccount"` + StakedBal string `json:"stakedBal"` + TransferChannelId string `json:"transferChannelId"` + UnbondingFrequency string `json:"unbondingFrequency"` + Validators []HostZoneValidator `json:"validators"` + BlacklistedValidators []HostZoneValidator `json:"blacklistedValidators"` +} + +type DepositRecord struct { + Id string `json:"id,omitempty"` + Amount string `json:"amount,omitempty"` + Denom string `json:"denom,omitempty"` + HostZoneId string `json:"hostZoneId,omitempty"` + Status string `json:"status,omitempty"` + DepositEpochNumber string `json:"depositEpochNumber,omitempty"` + Source string `json:"source,omitempty"` +} + +type DepositRecordWrapper struct { + DepositRecord []DepositRecord `json:"DepositRecord"` +} + +type UserRedemptionRecordWrapper struct { + UserRedemptionRecord []UserRedemptionRecord `json:"UserRedemptionRecord"` +} + +type UserRedemptionRecord struct { + ID string `json:"id"` + Sender string `json:"sender"` + Receiver string `json:"receiver"` + Amount string `json:"amount"` + Denom string `json:"denom"` + HostZoneID string `json:"hostZoneId"` + EpochNumber string `json:"epochNumber"` + ClaimIsPending bool `json:"claimIsPending"` +} + +func ModifyGenesisStride() func(ibc.ChainConfig, []byte) ([]byte, error) { + return func(cfg ibc.ChainConfig, genbz []byte) ([]byte, error) { + g := make(map[string]interface{}) + if err := json.Unmarshal(genbz, &g); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis file: %w", err) + } + + if err := dyno.Set(g, DayEpochLen, "app_state", "epochs", "epochs", DayEpochIndex, "duration"); err != nil { + return nil, err + } + if err := dyno.Set(g, StrideEpochLen, "app_state", "epochs", "epochs", StrideEpochIndex, "duration"); err != nil { + return nil, err + } + if err := dyno.Set(g, UnbondingTime, "app_state", "staking", "params", "unbonding_time"); err != nil { + return nil, err + } + if err := dyno.Set(g, IntervalLen, "app_state", "stakeibc", "params", "rewards_interval"); err != nil { + return nil, err + } + if err := dyno.Set(g, IntervalLen, "app_state", "stakeibc", "params", "delegate_interval"); err != nil { + return nil, err + } + if err := dyno.Set(g, IntervalLen, "app_state", "stakeibc", "params", "deposit_interval"); err != nil { + return nil, err + } + if err := dyno.Set(g, IntervalLen, "app_state", "stakeibc", "params", "redemption_rate_interval"); err != nil { + return nil, err + } + if err := dyno.Set(g, IntervalLen, "app_state", "stakeibc", "params", "reinvest_interval"); err != nil { + return nil, err + } + if err := dyno.Set(g, VotingPeriod, "app_state", "gov", "voting_params", "voting_period"); err != nil { + return nil, fmt.Errorf("failed to set voting period in genesis json: %w", err) + } + if err := dyno.Set(g, MaxDepositPeriod, "app_state", "gov", "deposit_params", "max_deposit_period"); err != nil { + return nil, fmt.Errorf("failed to set voting period in genesis json: %w", err) + } + + out, err := json.Marshal(g) + if err != nil { + return nil, fmt.Errorf("failed to marshal genesis bytes to json: %w", err) + } + return out, nil + } +} + +func ModifyGenesisStrideCounterparty() func(ibc.ChainConfig, []byte) ([]byte, error) { + return func(cfg ibc.ChainConfig, genbz []byte) ([]byte, error) { + g := make(map[string]interface{}) + if err := json.Unmarshal(genbz, &g); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis file: %w", err) + } + + if err := dyno.Set(g, UnbondingTime, + "app_state", "staking", "params", "unbonding_time", + ); err != nil { + return nil, err + } + + if err := dyno.Set(g, AllowMessages, + "app_state", "interchainaccounts", "host_genesis_state", "params", "allow_messages", + ); err != nil { + return nil, err + } + + out, err := json.Marshal(g) + if err != nil { + return nil, fmt.Errorf("failed to marshal genesis bytes to json: %w", err) + } + return out, nil + } +} + +// PollForMsgSubmitQueryResponse polls until finding a block with a MsgSubmitQueryResponse message +func PollForMsgSubmitQueryResponse( + ctx context.Context, + chain *cosmos.CosmosChain, + startHeight, maxHeight uint64, + chainID string, +) (*rlystride.MsgSubmitQueryResponse, error) { + cdc := codec.NewProtoCodec(chain.Config().EncodingConfig.InterfaceRegistry) + + doPoll := func(ctx context.Context, height uint64) (any, error) { + heightInt64 := int64(height) + block, err := chain.Validators[0].Client.Block(ctx, &heightInt64) + if err != nil { + return nil, err + } + + for _, tx := range block.Block.Txs { + sdkTx, err := authtx.DefaultTxDecoder(cdc)(tx) + if err != nil { + continue + } + for _, msg := range sdkTx.GetMsgs() { + if msgSubmitQueryResponse, ok := msg.(*rlystride.MsgSubmitQueryResponse); ok { + return msgSubmitQueryResponse, nil + } + } + } + return nil, fmt.Errorf("no MsgSubmitQueryResponse found") + } + bp := test.BlockPoller{CurrentHeight: chain.Height, PollFunc: doPoll} + p, err := bp.DoPoll(ctx, startHeight, maxHeight) + if err != nil { + return nil, err + } + return p.(*rlystride.MsgSubmitQueryResponse), nil +} + +// PollForMsgSubmitQueryResponse polls until finding a block with a MsgSubmitQueryResponse message +func PollForMsgChannelOpenConfirm( + ctx context.Context, + chain *cosmos.CosmosChain, + startHeight, maxHeight uint64, + chainID string, +) (*chantypes.MsgChannelOpenConfirm, error) { + cdc := codec.NewProtoCodec(chain.Config().EncodingConfig.InterfaceRegistry) + + doPoll := func(ctx context.Context, height uint64) (any, error) { + heightInt64 := int64(height) + block, err := chain.Validators[0].Client.Block(ctx, &heightInt64) + if err != nil { + return nil, err + } + + for _, tx := range block.Block.Txs { + sdkTx, err := authtx.DefaultTxDecoder(cdc)(tx) + if err != nil { + continue + } + for _, msg := range sdkTx.GetMsgs() { + if msgChannelOpenConfirm, ok := msg.(*chantypes.MsgChannelOpenConfirm); ok { + return msgChannelOpenConfirm, nil + } + } + } + return nil, fmt.Errorf("no MsgChannelOpenConfirm found") + } + bp := test.BlockPoller{CurrentHeight: chain.Height, PollFunc: doPoll} + p, err := bp.DoPoll(ctx, startHeight, maxHeight) + if err != nil { + return nil, err + } + return p.(*chantypes.MsgChannelOpenConfirm), nil +} diff --git a/ibctest/stride/stride_icq_test.go b/ibctest/stride/stride_icq_test.go new file mode 100644 index 000000000..b8af64d22 --- /dev/null +++ b/ibctest/stride/stride_icq_test.go @@ -0,0 +1,245 @@ +package stride_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v5/modules/apps/transfer/types" + relayeribctest "github.com/cosmos/relayer/v2/ibctest" + "github.com/cosmos/relayer/v2/ibctest/stride" + ibctest "github.com/strangelove-ventures/ibctest/v5" + "github.com/strangelove-ventures/ibctest/v5/chain/cosmos" + "github.com/strangelove-ventures/ibctest/v5/ibc" + "github.com/strangelove-ventures/ibctest/v5/test" + "github.com/strangelove-ventures/ibctest/v5/testreporter" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + "golang.org/x/sync/errgroup" +) + +// TestStrideICAandICQ is a test case that performs simulations and assertions around interchain accounts +// and the client implementation of interchain queries. See: https://github.com/Stride-Labs/interchain-queries +func TestScenarioStrideICAandICQ(t *testing.T) { + if testing.Short() { + t.Skip() + } + + client, network := ibctest.DockerSetup(t) + + rep := testreporter.NewNopReporter() + eRep := rep.RelayerExecReporter(t) + + ctx := context.Background() + + nf := 0 + nv := 1 + + // Define chains involved in test + cf := ibctest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*ibctest.ChainSpec{ + { + Name: "stride", + ChainName: "stride", + NumValidators: &nv, + NumFullNodes: &nf, + ChainConfig: ibc.ChainConfig{ + Type: "cosmos", + Name: "stride", + ChainID: "stride-1", + Images: []ibc.DockerImage{{ + Repository: "ghcr.io/strangelove-ventures/heighliner/stride", + Version: "andrew-test_admin_v5.1.1", + UidGid: "1025:1025", + }}, + Bin: "strided", + Bech32Prefix: "stride", + Denom: "ustrd", + GasPrices: "0.0ustrd", + TrustingPeriod: TrustingPeriod, + GasAdjustment: 1.1, + ModifyGenesis: ModifyGenesisStride(), + EncodingConfig: stride.Encoding(), + }}, + { + Name: "gaia", + ChainName: "gaia", + Version: "v8.0.0", + NumValidators: &nv, + NumFullNodes: &nf, + ChainConfig: ibc.ChainConfig{ + ModifyGenesis: ModifyGenesisStrideCounterparty(), + TrustingPeriod: TrustingPeriod, + }, + }, + }) + + chains, err := cf.Chains(t.Name()) + require.NoError(t, err) + + stride, gaia := chains[0].(*cosmos.CosmosChain), chains[1].(*cosmos.CosmosChain) + strideCfg, gaiaCfg := stride.Config(), gaia.Config() + + r := relayeribctest.NewRelayer(t, relayeribctest.RelayerConfig{}) + + // Build the network; spin up the chains and configure the relayer + const pathStrideGaia = "stride-gaia" + const relayerName = "relayer" + + clientOpts := ibc.DefaultClientOpts() + clientOpts.TrustingPeriod = TrustingPeriod + + ic := ibctest.NewInterchain(). + AddChain(stride). + AddChain(gaia). + AddRelayer(r, relayerName). + AddLink(ibctest.InterchainLink{ + Chain1: stride, + Chain2: gaia, + Relayer: r, + Path: pathStrideGaia, + CreateClientOpts: clientOpts, + }) + + require.NoError(t, ic.Build(ctx, eRep, ibctest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + // Uncomment this to load blocks, txs, msgs, and events into sqlite db as test runs + // BlockDatabaseFile: ibctest.DefaultBlockDatabaseFilepath(), + + SkipPathCreation: false, + })) + t.Cleanup(func() { + _ = ic.Close() + }) + + // Fund user accounts, so we can query balances and make assertions. + const userFunds = int64(10_000_000_000_000) + users := ibctest.GetAndFundTestUsers(t, ctx, t.Name(), userFunds, stride, gaia) + strideUser, gaiaUser := users[0], users[1] + + strideFullNode := stride.Validators[0] + + // Start the relayer + err = r.StartRelayer(ctx, eRep, pathStrideGaia) + require.NoError(t, err) + + t.Cleanup( + func() { + err := r.StopRelayer(ctx, eRep) + if err != nil { + t.Logf("an error occurred while stopping the relayer: %s", err) + } + }, + ) + + // Recover stride admin key + err = stride.RecoverKey(ctx, StrideAdminAccount, StrideAdminMnemonic) + require.NoError(t, err) + + strideAdminAddrBytes, err := stride.GetAddress(ctx, StrideAdminAccount) + require.NoError(t, err) + + strideAdminAddr, err := types.Bech32ifyAddressBytes(strideCfg.Bech32Prefix, strideAdminAddrBytes) + require.NoError(t, err) + + err = stride.SendFunds(ctx, ibctest.FaucetAccountKeyName, ibc.WalletAmount{ + Address: strideAdminAddr, + Amount: userFunds, + Denom: strideCfg.Denom, + }) + require.NoError(t, err, "failed to fund stride admin account") + + // get native chain user addresses + strideAddr := strideUser.Bech32Address(strideCfg.Bech32Prefix) + require.NotEmpty(t, strideAddr) + + gaiaAddress := gaiaUser.Bech32Address(gaiaCfg.Bech32Prefix) + require.NotEmpty(t, gaiaAddress) + + // get ibc paths + gaiaConns, err := r.GetConnections(ctx, eRep, gaiaCfg.ChainID) + require.NoError(t, err) + + gaiaChans, err := r.GetChannels(ctx, eRep, gaiaCfg.ChainID) + require.NoError(t, err) + + atomIBCDenom := transfertypes.ParseDenomTrace( + transfertypes.GetPrefixedDenom( + gaiaChans[0].Counterparty.PortID, + gaiaChans[0].Counterparty.ChannelID, + gaiaCfg.Denom, + ), + ).IBCDenom() + + var eg errgroup.Group + + // Fund stride user with ibc transfers + gaiaHeight, err := gaia.Height(ctx) + require.NoError(t, err) + + // Fund stride user with ibc denom atom + tx, err := gaia.SendIBCTransfer(ctx, gaiaChans[0].ChannelID, gaiaUser.KeyName, ibc.WalletAmount{ + Amount: 1_000_000_000_000, + Denom: gaiaCfg.Denom, + Address: strideAddr, + }, nil) + require.NoError(t, err) + + _, err = test.PollForAck(ctx, gaia, gaiaHeight, gaiaHeight+10, tx.Packet) + require.NoError(t, err) + + require.NoError(t, eg.Wait()) + + // Register gaia host zone + _, err = strideFullNode.ExecTx(ctx, StrideAdminAccount, + "stakeibc", "register-host-zone", + gaiaConns[0].Counterparty.ConnectionId, gaiaCfg.Denom, gaiaCfg.Bech32Prefix, + atomIBCDenom, gaiaChans[0].Counterparty.ChannelID, "1", + "--gas", "1000000", + ) + require.NoError(t, err) + + gaiaHeight, err = gaia.Height(ctx) + require.NoError(t, err) + + // Wait for the ICA accounts to be setup + _, err = PollForMsgChannelOpenConfirm(ctx, gaia, gaiaHeight, gaiaHeight+15, gaiaCfg.ChainID) + require.NoError(t, err) + + // Get validator address + gaiaVal1Address, err := gaia.Validators[0].KeyBech32(ctx, "validator", "val") + require.NoError(t, err) + + // Add gaia validator + _, err = strideFullNode.ExecTx(ctx, StrideAdminAccount, + "stakeibc", "add-validator", + gaiaCfg.ChainID, "gval1", gaiaVal1Address, + "10", "5", + ) + require.NoError(t, err) + + var gaiaHostZone HostZoneWrapper + + // query gaia host zone + stdout, _, err := strideFullNode.ExecQuery(ctx, + "stakeibc", "show-host-zone", gaiaCfg.ChainID, + ) + require.NoError(t, err) + err = json.Unmarshal(stdout, &gaiaHostZone) + require.NoError(t, err) + + // Liquid stake some atom + _, err = strideFullNode.ExecTx(ctx, strideUser.KeyName, + "stakeibc", "liquid-stake", + "1000000000000", gaiaCfg.Denom, + ) + require.NoError(t, err) + + strideHeight, err := stride.Height(ctx) + require.NoError(t, err) + + _, err = PollForMsgSubmitQueryResponse(ctx, stride, strideHeight, strideHeight+20, strideCfg.ChainID) + require.NoError(t, err) +} diff --git a/proto/stride/interchainquery/v1/messages.proto b/proto/stride/interchainquery/v1/messages.proto new file mode 100644 index 000000000..8c974d9a6 --- /dev/null +++ b/proto/stride/interchainquery/v1/messages.proto @@ -0,0 +1,25 @@ + +syntax = "proto3"; +package stride.interchainquery.v1; + +import "gogoproto/gogo.proto"; +import "cosmos_proto/cosmos.proto"; +import "tendermint/crypto/proof.proto"; + +// NOTE: copied into this repository from "github.com/Stride-Labs/stride/v5/x/interchainquery/types" +option go_package = "github.com/cosmos/relayer/relayer/chains/cosmos/stride"; + +// MsgSubmitQueryResponse represents a message type to fulfil a query request. +message MsgSubmitQueryResponse { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + option (gogoproto.goproto_stringer) = true; + + string chain_id = 1 [ (gogoproto.moretags) = "yaml:\"chain_id\"" ]; + string query_id = 2 [ (gogoproto.moretags) = "yaml:\"query_id\"" ]; + bytes result = 3 [ (gogoproto.moretags) = "yaml:\"result\"" ]; + tendermint.crypto.ProofOps proof_ops = 4 + [ (gogoproto.moretags) = "yaml:\"proof_ops\"" ]; + int64 height = 5 [ (gogoproto.moretags) = "yaml:\"height\"" ]; + string from_address = 6 [ (cosmos_proto.scalar) = "cosmos.AddressString" ]; +} diff --git a/relayer/chains/cosmos/event_parser.go b/relayer/chains/cosmos/event_parser.go index ddc321dc9..9e570c43a 100644 --- a/relayer/chains/cosmos/event_parser.go +++ b/relayer/chains/cosmos/event_parser.go @@ -2,6 +2,7 @@ package cosmos import ( "encoding/hex" + "fmt" "strconv" "strings" @@ -103,6 +104,17 @@ func parseIBCMessageFromEvent( eventType: event.Type, info: ci, } + + case string(processor.ClientICQTypeRequest), string(processor.ClientICQTypeResponse): + ci := &clientICQInfo{ + Height: height, + Source: chainID, + } + ci.parseAttrs(log, event.Attributes) + return &ibcMessage{ + eventType: event.Type, + info: ci, + } } return nil } @@ -379,3 +391,56 @@ func (res *connectionInfo) parseConnectionAttribute(attr sdk.Attribute) { res.CounterpartyClientID = attr.Value } } + +type clientICQInfo struct { + Source string + Connection string + Chain string + QueryID provider.ClientICQQueryID + Type string + Request []byte + Height uint64 +} + +func (res *clientICQInfo) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("connection_id", res.Connection) + enc.AddString("chain_id", res.Chain) + enc.AddString("query_id", string(res.QueryID)) + enc.AddString("type", res.Type) + enc.AddString("request", hex.EncodeToString(res.Request)) + enc.AddUint64("height", res.Height) + + return nil +} + +func (res *clientICQInfo) parseAttrs(log *zap.Logger, attrs []sdk.Attribute) { + for _, attr := range attrs { + if err := res.parseAttribute(attr); err != nil { + panic(fmt.Errorf("failed to parse attributes from client ICQ message: %w", err)) + } + } +} + +func (res *clientICQInfo) parseAttribute(attr sdk.Attribute) (err error) { + switch attr.Key { + case "connection_id": + res.Connection = attr.Value + case "chain_id": + res.Chain = attr.Value + case "query_id": + res.QueryID = provider.ClientICQQueryID(attr.Value) + case "type": + res.Type = attr.Value + case "request": + res.Request, err = hex.DecodeString(attr.Value) + if err != nil { + return err + } + case "height": + res.Height, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return err + } + } + return nil +} diff --git a/relayer/chains/cosmos/message_handlers.go b/relayer/chains/cosmos/message_handlers.go index b28da07b2..92ee332bf 100644 --- a/relayer/chains/cosmos/message_handlers.go +++ b/relayer/chains/cosmos/message_handlers.go @@ -2,6 +2,7 @@ package cosmos import ( "context" + "encoding/hex" conntypes "github.com/cosmos/ibc-go/v5/modules/core/03-connection/types" chantypes "github.com/cosmos/ibc-go/v5/modules/core/04-channel/types" @@ -21,6 +22,8 @@ func (ccp *CosmosChainProcessor) handleMessage(ctx context.Context, m ibcMessage ccp.handleConnectionMessage(m.eventType, provider.ConnectionInfo(*t), c) case *clientInfo: ccp.handleClientMessage(ctx, m.eventType, *t) + case *clientICQInfo: + ccp.handleClientICQMessage(m.eventType, provider.ClientICQInfo(*t), c) } } @@ -133,6 +136,15 @@ func (ccp *CosmosChainProcessor) handleClientMessage(ctx context.Context, eventT ccp.logObservedIBCMessage(eventType, zap.String("client_id", ci.clientID)) } +func (ccp *CosmosChainProcessor) handleClientICQMessage( + eventType string, + ci provider.ClientICQInfo, + c processor.IBCMessagesCache, +) { + c.ClientICQ.Retain(processor.ClientICQType(eventType), ci) + ccp.logClientICQMessage(eventType, ci) +} + func (ccp *CosmosChainProcessor) logObservedIBCMessage(m string, fields ...zap.Field) { ccp.log.With(zap.String("event_type", m)).Debug("Observed IBC message", fields...) } @@ -178,3 +190,14 @@ func (ccp *CosmosChainProcessor) logConnectionMessage(message string, ci provide zap.String("counterparty_connection_id", ci.CounterpartyConnID), ) } + +func (ccp *CosmosChainProcessor) logClientICQMessage(icqType string, ci provider.ClientICQInfo) { + ccp.logObservedIBCMessage(icqType, + zap.String("type", ci.Type), + zap.String("query_id", string(ci.QueryID)), + zap.String("request", hex.EncodeToString(ci.Request)), + zap.String("chain_id", ci.Chain), + zap.String("connection_id", ci.Connection), + zap.Uint64("height", ci.Height), + ) +} diff --git a/relayer/chains/cosmos/provider.go b/relayer/chains/cosmos/provider.go index b13e6537a..d4cb7eaae 100644 --- a/relayer/chains/cosmos/provider.go +++ b/relayer/chains/cosmos/provider.go @@ -8,10 +8,10 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/module" commitmenttypes "github.com/cosmos/ibc-go/v5/modules/core/23-commitment/types" ibcexported "github.com/cosmos/ibc-go/v5/modules/core/exported" tmclient "github.com/cosmos/ibc-go/v5/modules/light-clients/07-tendermint/types" + "github.com/cosmos/relayer/v2/relayer/chains/cosmos/stride" "github.com/cosmos/relayer/v2/relayer/processor" "github.com/cosmos/relayer/v2/relayer/provider" "github.com/gogo/protobuf/proto" @@ -77,6 +77,8 @@ func (pc CosmosProviderConfig) NewProvider(log *zap.Logger, homepath string, deb // ChainClientConfig builds a ChainClientConfig struct from a CosmosProviderConfig, this is used // to instantiate an instance of ChainClient from lens which is how we build the CosmosProvider func ChainClientConfig(pcfg *CosmosProviderConfig) *lens.ChainClientConfig { + modules := lens.ModuleBasics + modules = append(modules, stride.AppModuleBasic{}) return &lens.ChainClientConfig{ Key: pcfg.Key, ChainID: pcfg.ChainID, @@ -91,7 +93,7 @@ func ChainClientConfig(pcfg *CosmosProviderConfig) *lens.ChainClientConfig { OutputFormat: pcfg.OutputFormat, SignModeStr: pcfg.SignModeStr, ExtraCodecs: pcfg.ExtraCodecs, - Modules: append([]module.AppModuleBasic{}, lens.ModuleBasics...), + Modules: modules, } } @@ -218,13 +220,13 @@ func (cc *CosmosProvider) TrustingPeriod(ctx context.Context) (time.Duration, er // and then re-growing by 85x. tp := unbondingTime / 100 * 85 - // We only want the trusting period to be whole hours unless it's less than an hour (for testing). - truncated := tp.Truncate(time.Hour) - if truncated.Hours() == 0 { - return tp, nil + // And we only want the trusting period to be whole hours. + // But avoid rounding if the time is less than 1 hour + // (otherwise the trusting period will go to 0) + if tp > time.Hour { + tp = tp.Truncate(time.Hour) } - - return truncated, nil + return tp, nil } // Sprint returns the json representation of the specified proto message. diff --git a/relayer/chains/cosmos/stride/codec.go b/relayer/chains/cosmos/stride/codec.go new file mode 100644 index 000000000..58c48b9e5 --- /dev/null +++ b/relayer/chains/cosmos/stride/codec.go @@ -0,0 +1,32 @@ +package stride + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Originally sourced from https://github.com/Stride-Labs/stride/blob/v5.1.1/x/interchainquery/types/codec.go +// Needed for cosmos sdk Msg implementation in messages.go. + +var ( + amino = codec.NewLegacyAmino() + ModuleCdc = codec.NewAminoCodec(amino) +) + +func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + cdc.RegisterConcrete(&MsgSubmitQueryResponse{}, "/stride.interchainquery.MsgSubmitQueryResponse", nil) +} + +func RegisterInterfaces(registry types.InterfaceRegistry) { + registry.RegisterImplementations((*sdk.Msg)(nil), + &MsgSubmitQueryResponse{}, + ) +} + +func init() { + RegisterLegacyAminoCodec(amino) + cryptocodec.RegisterCrypto(amino) + amino.Seal() +} diff --git a/relayer/chains/cosmos/stride/messages.go b/relayer/chains/cosmos/stride/messages.go new file mode 100644 index 000000000..29bdf5ed7 --- /dev/null +++ b/relayer/chains/cosmos/stride/messages.go @@ -0,0 +1,51 @@ +package stride + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// Originally sourced from https://github.com/Stride-Labs/stride/blob/v5.1.1/x/interchainquery/types/msgs.go +// Needed for cosmos sdk Msg implementation. + +// interchainquery message types +const ( + TypeMsgSubmitQueryResponse = "submitqueryresponse" + + // RouterKey is the message route for icq + RouterKey = "interchainquery" +) + +var _ sdk.Msg = &MsgSubmitQueryResponse{} + +// Route Implements Msg. +func (msg MsgSubmitQueryResponse) Route() string { return RouterKey } + +// Type Implements Msg. +func (msg MsgSubmitQueryResponse) Type() string { return TypeMsgSubmitQueryResponse } + +// ValidateBasic Implements Msg. +func (msg MsgSubmitQueryResponse) ValidateBasic() error { + // check from address + _, err := sdk.AccAddressFromBech32(msg.FromAddress) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid fromAddress in ICQ response (%s)", err) + } + // check chain_id is not empty + if msg.ChainId == "" { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "chain_id cannot be empty in ICQ response") + } + + return nil +} + +// GetSignBytes Implements Msg. +func (msg MsgSubmitQueryResponse) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +// GetSigners Implements Msg. +func (msg MsgSubmitQueryResponse) GetSigners() []sdk.AccAddress { + fromAddress, _ := sdk.AccAddressFromBech32(msg.FromAddress) + return []sdk.AccAddress{fromAddress} +} diff --git a/relayer/chains/cosmos/stride/messages.pb.go b/relayer/chains/cosmos/stride/messages.pb.go new file mode 100644 index 000000000..b22c6434f --- /dev/null +++ b/relayer/chains/cosmos/stride/messages.pb.go @@ -0,0 +1,544 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: stride/interchainquery/v1/messages.proto + +package stride + +import ( + fmt "fmt" + _ "github.com/cosmos/cosmos-proto" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + crypto "github.com/tendermint/tendermint/proto/tendermint/crypto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// MsgSubmitQueryResponse represents a message type to fulfil a query request. +type MsgSubmitQueryResponse struct { + ChainId string `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty" yaml:"chain_id"` + QueryId string `protobuf:"bytes,2,opt,name=query_id,json=queryId,proto3" json:"query_id,omitempty" yaml:"query_id"` + Result []byte `protobuf:"bytes,3,opt,name=result,proto3" json:"result,omitempty" yaml:"result"` + ProofOps *crypto.ProofOps `protobuf:"bytes,4,opt,name=proof_ops,json=proofOps,proto3" json:"proof_ops,omitempty" yaml:"proof_ops"` + Height int64 `protobuf:"varint,5,opt,name=height,proto3" json:"height,omitempty" yaml:"height"` + FromAddress string `protobuf:"bytes,6,opt,name=from_address,json=fromAddress,proto3" json:"from_address,omitempty"` +} + +func (m *MsgSubmitQueryResponse) Reset() { *m = MsgSubmitQueryResponse{} } +func (m *MsgSubmitQueryResponse) String() string { return proto.CompactTextString(m) } +func (*MsgSubmitQueryResponse) ProtoMessage() {} +func (*MsgSubmitQueryResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_25adad4f8ed32400, []int{0} +} +func (m *MsgSubmitQueryResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgSubmitQueryResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgSubmitQueryResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgSubmitQueryResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgSubmitQueryResponse.Merge(m, src) +} +func (m *MsgSubmitQueryResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgSubmitQueryResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgSubmitQueryResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgSubmitQueryResponse proto.InternalMessageInfo + +func init() { + proto.RegisterType((*MsgSubmitQueryResponse)(nil), "stride.interchainquery.v1.MsgSubmitQueryResponse") +} + +func init() { + proto.RegisterFile("stride/interchainquery/v1/messages.proto", fileDescriptor_25adad4f8ed32400) +} + +var fileDescriptor_25adad4f8ed32400 = []byte{ + // 415 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x92, 0x3f, 0x0f, 0x93, 0x40, + 0x18, 0x87, 0xb9, 0x56, 0xfb, 0x87, 0xd6, 0xa8, 0xd8, 0x18, 0x5a, 0x23, 0x10, 0x26, 0x1c, 0x3c, + 0x52, 0x4d, 0x1c, 0xea, 0x24, 0x5b, 0x07, 0xb5, 0xd2, 0xcd, 0x85, 0x50, 0xb8, 0xc2, 0x25, 0x85, + 0xc3, 0xbb, 0xa3, 0x09, 0xdf, 0xc0, 0xd1, 0xd1, 0xb1, 0x1f, 0xc2, 0x0f, 0xe1, 0xd8, 0x38, 0x39, + 0x35, 0xa6, 0x5d, 0x74, 0xed, 0x27, 0x30, 0xdc, 0xd1, 0x6a, 0x3a, 0xf1, 0xf2, 0x3e, 0xcf, 0xf1, + 0xbb, 0xf7, 0x0e, 0xd5, 0x61, 0x9c, 0xe2, 0x18, 0xb9, 0x38, 0xe7, 0x88, 0x46, 0x69, 0x88, 0xf3, + 0x4f, 0x25, 0xa2, 0x95, 0xbb, 0x9d, 0xba, 0x19, 0x62, 0x2c, 0x4c, 0x10, 0x83, 0x05, 0x25, 0x9c, + 0x68, 0x63, 0x69, 0xc2, 0x1b, 0x13, 0x6e, 0xa7, 0x93, 0x51, 0x42, 0x12, 0x22, 0x2c, 0xb7, 0xae, + 0xe4, 0x82, 0xc9, 0x38, 0x22, 0x2c, 0x23, 0x2c, 0x90, 0x40, 0xbe, 0x34, 0xe8, 0x29, 0x47, 0x79, + 0x8c, 0x68, 0x86, 0x73, 0xee, 0x46, 0xb4, 0x2a, 0x38, 0x71, 0x0b, 0x4a, 0xc8, 0x5a, 0x62, 0xfb, + 0x4f, 0x4b, 0x7d, 0xfc, 0x96, 0x25, 0xcb, 0x72, 0x95, 0x61, 0xfe, 0xa1, 0x4e, 0xf1, 0x11, 0x2b, + 0x48, 0xce, 0x90, 0x06, 0xd5, 0x9e, 0xc8, 0x0e, 0x70, 0xac, 0x03, 0x0b, 0x38, 0x7d, 0xef, 0xd1, + 0xf9, 0x60, 0xde, 0xaf, 0xc2, 0x6c, 0x33, 0xb3, 0x2f, 0xc4, 0xf6, 0xbb, 0xa2, 0x9c, 0xc7, 0xb5, + 0x2f, 0xb6, 0x59, 0xfb, 0xad, 0x5b, 0xff, 0x42, 0x6c, 0xbf, 0x2b, 0xca, 0x79, 0xac, 0x3d, 0x53, + 0x3b, 0x14, 0xb1, 0x72, 0xc3, 0xf5, 0xb6, 0x05, 0x9c, 0xa1, 0xf7, 0xf0, 0x7c, 0x30, 0xef, 0x49, + 0x5b, 0xf6, 0x6d, 0xbf, 0x11, 0xb4, 0x77, 0x6a, 0x5f, 0x6c, 0x3a, 0x20, 0x05, 0xd3, 0xef, 0x58, + 0xc0, 0x19, 0xbc, 0x78, 0x02, 0xff, 0x0d, 0x06, 0xe5, 0x60, 0x70, 0x51, 0x3b, 0xef, 0x0b, 0xe6, + 0x8d, 0xce, 0x07, 0xf3, 0x81, 0xfc, 0xd4, 0x75, 0x9d, 0xed, 0xf7, 0x8a, 0x86, 0xd7, 0xd1, 0x29, + 0xc2, 0x49, 0xca, 0xf5, 0xbb, 0x16, 0x70, 0xda, 0xff, 0x47, 0xcb, 0xbe, 0xed, 0x37, 0x82, 0xf6, + 0x5a, 0x1d, 0xae, 0x29, 0xc9, 0x82, 0x30, 0x8e, 0x29, 0x62, 0x4c, 0xef, 0x88, 0xc9, 0xf4, 0x1f, + 0xdf, 0x9e, 0x8f, 0x9a, 0x73, 0x7e, 0x23, 0xc9, 0x92, 0x53, 0x9c, 0x27, 0xfe, 0xa0, 0xb6, 0x9b, + 0xd6, 0x6c, 0xf8, 0x79, 0x67, 0x2a, 0x5f, 0x77, 0x26, 0xf8, 0xbd, 0x33, 0x15, 0x6f, 0xf1, 0xfd, + 0x68, 0x80, 0xfd, 0xd1, 0x00, 0xbf, 0x8e, 0x06, 0xf8, 0x72, 0x32, 0x94, 0xfd, 0xc9, 0x50, 0x7e, + 0x9e, 0x0c, 0xe5, 0xe3, 0xab, 0x04, 0xf3, 0xb4, 0x5c, 0xc1, 0x88, 0x64, 0xcd, 0xed, 0xb9, 0x14, + 0x6d, 0xc2, 0x0a, 0xd1, 0xeb, 0x53, 0x9c, 0x32, 0xbb, 0x50, 0xf9, 0x83, 0xac, 0x3a, 0xe2, 0x12, + 0x5f, 0xfe, 0x0d, 0x00, 0x00, 0xff, 0xff, 0x5e, 0x6b, 0x0e, 0x66, 0x5b, 0x02, 0x00, 0x00, +} + +func (m *MsgSubmitQueryResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgSubmitQueryResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgSubmitQueryResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.FromAddress) > 0 { + i -= len(m.FromAddress) + copy(dAtA[i:], m.FromAddress) + i = encodeVarintMessages(dAtA, i, uint64(len(m.FromAddress))) + i-- + dAtA[i] = 0x32 + } + if m.Height != 0 { + i = encodeVarintMessages(dAtA, i, uint64(m.Height)) + i-- + dAtA[i] = 0x28 + } + if m.ProofOps != nil { + { + size, err := m.ProofOps.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMessages(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + if len(m.Result) > 0 { + i -= len(m.Result) + copy(dAtA[i:], m.Result) + i = encodeVarintMessages(dAtA, i, uint64(len(m.Result))) + i-- + dAtA[i] = 0x1a + } + if len(m.QueryId) > 0 { + i -= len(m.QueryId) + copy(dAtA[i:], m.QueryId) + i = encodeVarintMessages(dAtA, i, uint64(len(m.QueryId))) + i-- + dAtA[i] = 0x12 + } + if len(m.ChainId) > 0 { + i -= len(m.ChainId) + copy(dAtA[i:], m.ChainId) + i = encodeVarintMessages(dAtA, i, uint64(len(m.ChainId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintMessages(dAtA []byte, offset int, v uint64) int { + offset -= sovMessages(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *MsgSubmitQueryResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.ChainId) + if l > 0 { + n += 1 + l + sovMessages(uint64(l)) + } + l = len(m.QueryId) + if l > 0 { + n += 1 + l + sovMessages(uint64(l)) + } + l = len(m.Result) + if l > 0 { + n += 1 + l + sovMessages(uint64(l)) + } + if m.ProofOps != nil { + l = m.ProofOps.Size() + n += 1 + l + sovMessages(uint64(l)) + } + if m.Height != 0 { + n += 1 + sovMessages(uint64(m.Height)) + } + l = len(m.FromAddress) + if l > 0 { + n += 1 + l + sovMessages(uint64(l)) + } + return n +} + +func sovMessages(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozMessages(x uint64) (n int) { + return sovMessages(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *MsgSubmitQueryResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMessages + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgSubmitQueryResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgSubmitQueryResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ChainId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMessages + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthMessages + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthMessages + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ChainId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field QueryId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMessages + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthMessages + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthMessages + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.QueryId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Result", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMessages + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthMessages + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthMessages + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Result = append(m.Result[:0], dAtA[iNdEx:postIndex]...) + if m.Result == nil { + m.Result = []byte{} + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ProofOps", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMessages + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMessages + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMessages + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.ProofOps == nil { + m.ProofOps = &crypto.ProofOps{} + } + if err := m.ProofOps.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Height", wireType) + } + m.Height = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMessages + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Height |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field FromAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMessages + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthMessages + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthMessages + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.FromAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipMessages(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMessages + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipMessages(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMessages + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMessages + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMessages + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthMessages + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupMessages + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthMessages + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthMessages = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowMessages = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupMessages = fmt.Errorf("proto: unexpected end of group") +) diff --git a/relayer/chains/cosmos/stride/module.go b/relayer/chains/cosmos/stride/module.go new file mode 100644 index 000000000..b7a6ef61b --- /dev/null +++ b/relayer/chains/cosmos/stride/module.go @@ -0,0 +1,50 @@ +package stride + +import ( + "encoding/json" + + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/types/module" +) + +var ( + _ module.AppModuleBasic = AppModuleBasic{} +) + +// AppModuleBasic implements the AppModuleBasic interface +type AppModuleBasic struct{} + +func (AppModuleBasic) Name() string { + return "interchainquery" +} + +func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + RegisterLegacyAminoCodec(cdc) +} + +func (a AppModuleBasic) RegisterInterfaces(reg cdctypes.InterfaceRegistry) { + RegisterInterfaces(reg) +} + +func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + return nil +} + +func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error { + return nil +} + +func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) {} + +func (a AppModuleBasic) GetTxCmd() *cobra.Command { + return nil +} + +func (AppModuleBasic) GetQueryCmd() *cobra.Command { + return nil +} diff --git a/relayer/chains/cosmos/tx.go b/relayer/chains/cosmos/tx.go index 3f71669cc..f0edda37a 100644 --- a/relayer/chains/cosmos/tx.go +++ b/relayer/chains/cosmos/tx.go @@ -24,7 +24,9 @@ import ( host "github.com/cosmos/ibc-go/v5/modules/core/24-host" ibcexported "github.com/cosmos/ibc-go/v5/modules/core/exported" tmclient "github.com/cosmos/ibc-go/v5/modules/light-clients/07-tendermint/types" + strideicqtypes "github.com/cosmos/relayer/v2/relayer/chains/cosmos/stride" "github.com/cosmos/relayer/v2/relayer/provider" + abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/light" tmtypes "github.com/tendermint/tendermint/types" "go.uber.org/zap" @@ -891,6 +893,43 @@ func (cc *CosmosProvider) MsgUpdateClientHeader(latestHeader provider.IBCHeader, }, nil } +func (cc *CosmosProvider) QueryICQWithProof(ctx context.Context, path string, request []byte, height uint64) (provider.ICQProof, error) { + slashSplit := strings.Split(path, "/") + req := abci.RequestQuery{ + Path: path, + Height: int64(height), + Data: request, + Prove: slashSplit[len(slashSplit)-1] == "key", + } + + res, err := cc.QueryABCI(ctx, req) + if err != nil { + return provider.ICQProof{}, fmt.Errorf("failed to execute interchain query: %w", err) + } + return provider.ICQProof{ + Result: res.Value, + ProofOps: res.ProofOps, + Height: res.Height, + }, nil +} + +func (cc *CosmosProvider) MsgSubmitQueryResponse(chainID string, queryID provider.ClientICQQueryID, proof provider.ICQProof) (provider.RelayerMessage, error) { + signer, err := cc.Address() + if err != nil { + return nil, err + } + msg := &strideicqtypes.MsgSubmitQueryResponse{ + ChainId: chainID, + QueryId: string(queryID), + Result: proof.Result, + ProofOps: proof.ProofOps, + Height: proof.Height, + FromAddress: signer, + } + + return NewCosmosMessage(msg), nil +} + // RelayPacketFromSequence relays a packet with a given seq on src and returns recvPacket msgs, timeoutPacketmsgs and error func (cc *CosmosProvider) RelayPacketFromSequence( ctx context.Context, diff --git a/relayer/processor/path_end_runtime.go b/relayer/processor/path_end_runtime.go index 83da731fe..3e1770518 100644 --- a/relayer/processor/path_end_runtime.go +++ b/relayer/processor/path_end_runtime.go @@ -35,9 +35,10 @@ type pathEndRuntime struct { // Cache in progress sends for the different IBC message types // to avoid retrying a message immediately after it is sent. - packetProcessing packetProcessingCache - connProcessing connectionProcessingCache - channelProcessing channelProcessingCache + packetProcessing packetProcessingCache + connProcessing connectionProcessingCache + channelProcessing channelProcessingCache + clientICQProcessing clientICQProcessingCache // Message subscriber callbacks connSubscribers map[string][]func(provider.ConnectionInfo) @@ -64,6 +65,7 @@ func newPathEndRuntime(log *zap.Logger, pathEnd PathEnd, metrics *PrometheusMetr packetProcessing: make(packetProcessingCache), connProcessing: make(connectionProcessingCache), channelProcessing: make(channelProcessingCache), + clientICQProcessing: make(clientICQProcessingCache), connSubscribers: make(map[string][]func(provider.ConnectionInfo)), metrics: metrics, } @@ -94,6 +96,7 @@ func (pathEnd *pathEndRuntime) mergeMessageCache(messageCache IBCMessagesCache, packetMessages := make(ChannelPacketMessagesCache) connectionHandshakeMessages := make(ConnectionMessagesCache) channelHandshakeMessages := make(ChannelMessagesCache) + clientICQMessages := make(ClientICQMessagesCache) for ch, pmc := range messageCache.PacketFlow { if pathEnd.info.ShouldRelayChannel(ChainChannelKey{ChainID: pathEnd.info.ChainID, CounterpartyChainID: counterpartyChainID, ChannelKey: ch}) { @@ -149,6 +152,17 @@ func (pathEnd *pathEndRuntime) mergeMessageCache(messageCache IBCMessagesCache, channelHandshakeMessages[eventType] = newCmc } pathEnd.messageCache.ChannelHandshake.Merge(channelHandshakeMessages) + + for icqType, cm := range messageCache.ClientICQ { + newCache := make(ClientICQMessageCache) + for queryID, m := range cm { + if m.Chain == counterpartyChainID { + newCache[queryID] = m + } + } + clientICQMessages[icqType] = newCache + } + pathEnd.messageCache.ClientICQ.Merge(clientICQMessages) } func (pathEnd *pathEndRuntime) handleCallbacks(c IBCMessagesCache) { @@ -572,6 +586,45 @@ func (pathEnd *pathEndRuntime) shouldSendChannelMessage(message channelIBCMessag return true } +// shouldSendClientICQMessage determines if the client ICQ message should be sent now. +// It will also determine if the message needs to be given up on entirely and remove retention if so. +func (pathEnd *pathEndRuntime) shouldSendClientICQMessage(message provider.ClientICQInfo) bool { + queryID := message.QueryID + inProgress, ok := pathEnd.clientICQProcessing[queryID] + if !ok { + // in progress cache does not exist for this query ID, so can send. + return true + } + blocksSinceLastProcessed := pathEnd.latestBlock.Height - inProgress.lastProcessedHeight + if inProgress.assembled { + if blocksSinceLastProcessed < blocksToRetrySendAfter { + // this message was sent less than blocksToRetrySendAfter ago, do not attempt to send again yet. + return false + } + } else { + if blocksSinceLastProcessed < blocksToRetryAssemblyAfter { + // this message was sent less than blocksToRetryAssemblyAfter ago, do not attempt assembly again yet. + return false + } + } + if inProgress.retryCount >= maxMessageSendRetries { + pathEnd.log.Error("Giving up on sending client ICQ message after max retries", + zap.String("query_id", string(queryID)), + ) + + // giving up on this query + // remove all retention of this client interchain query flow in pathEnd.messagesCache.ClientICQ + pathEnd.messageCache.ClientICQ.DeleteMessages(queryID) + + // delete in progress query for this specific ID + delete(pathEnd.clientICQProcessing, queryID) + + return false + } + + return true +} + func (pathEnd *pathEndRuntime) trackProcessingPacketMessage(t packetMessageToTrack) { eventType := t.msg.eventType sequence := t.msg.info.Sequence @@ -652,3 +705,19 @@ func (pathEnd *pathEndRuntime) trackProcessingChannelMessage(t channelMessageToT assembled: t.assembled, } } + +func (pathEnd *pathEndRuntime) trackProcessingClientICQMessage(t clientICQMessageToTrack) { + retryCount := uint64(0) + + queryID := t.msg.info.QueryID + + if inProgress, ok := pathEnd.clientICQProcessing[queryID]; ok { + retryCount = inProgress.retryCount + 1 + } + + pathEnd.clientICQProcessing[queryID] = processingMessage{ + lastProcessedHeight: pathEnd.latestBlock.Height, + retryCount: retryCount, + assembled: t.assembled, + } +} diff --git a/relayer/processor/path_processor.go b/relayer/processor/path_processor.go index 1b76b6bd1..c21af22a0 100644 --- a/relayer/processor/path_processor.go +++ b/relayer/processor/path_processor.go @@ -24,6 +24,9 @@ const ( // to be relayed. packetProofQueryTimeout = 5 * time.Second + // Amount of time to wait for interchain queries. + interchainQueryTimeout = 60 * time.Second + // If message assembly fails from either proof query failure on the source // or assembling the message for the destination, how many blocks should pass // before retrying. diff --git a/relayer/processor/path_processor_internal.go b/relayer/processor/path_processor_internal.go index cdd18d357..761bb5bb5 100644 --- a/relayer/processor/path_processor_internal.go +++ b/relayer/processor/path_processor_internal.go @@ -395,6 +395,33 @@ ChannelHandshakeLoop: return res } +func (pp *PathProcessor) getUnrelayedClientICQMessages(pathEnd *pathEndRuntime, queryMessages, responseMessages ClientICQMessageCache) (res []clientICQMessage) { +ClientICQLoop: + for queryID, queryMsg := range queryMessages { + for resQueryID := range responseMessages { + if queryID == resQueryID { + // done with this query, remove all retention. + pathEnd.messageCache.ClientICQ.DeleteMessages(queryID) + delete(pathEnd.clientICQProcessing, queryID) + continue ClientICQLoop + } + } + // query ID not found in response messages, check if should send queryMsg and send + if pathEnd.shouldSendClientICQMessage(queryMsg) { + res = append(res, clientICQMessage{ + info: queryMsg, + }) + } + } + + // now iterate through completion message and remove any leftover messages. + for queryID := range responseMessages { + pathEnd.messageCache.ClientICQ.DeleteMessages(queryID) + delete(pathEnd.clientICQProcessing, queryID) + } + return res +} + // assembleMsgUpdateClient uses the ChainProvider from both pathEnds to assemble the client update header // from the source and then assemble the update client message in the correct format for the destination. func (pp *PathProcessor) assembleMsgUpdateClient(ctx context.Context, src, dst *pathEndRuntime) (provider.RelayerMessage, error) { @@ -660,16 +687,29 @@ func (pp *PathProcessor) processLatestMessages(ctx context.Context, messageLifec pathEnd1ChannelMessages = append(pathEnd1ChannelMessages, pathEnd1ChanCloseMessages...) pathEnd2ChannelMessages = append(pathEnd2ChannelMessages, pathEnd2ChanCloseMessages...) + pathEnd1ClientICQMessages := pp.getUnrelayedClientICQMessages( + pp.pathEnd1, + pp.pathEnd1.messageCache.ClientICQ[ClientICQTypeRequest], + pp.pathEnd1.messageCache.ClientICQ[ClientICQTypeResponse], + ) + pathEnd2ClientICQMessages := pp.getUnrelayedClientICQMessages( + pp.pathEnd2, + pp.pathEnd2.messageCache.ClientICQ[ClientICQTypeRequest], + pp.pathEnd2.messageCache.ClientICQ[ClientICQTypeResponse], + ) + pathEnd1Messages := pathEndMessages{ connectionMessages: pathEnd1ConnectionMessages, channelMessages: pathEnd1ChannelMessages, packetMessages: pathEnd1PacketMessages, + clientICQMessages: pathEnd1ClientICQMessages, } pathEnd2Messages := pathEndMessages{ connectionMessages: pathEnd2ConnectionMessages, channelMessages: pathEnd2ChannelMessages, packetMessages: pathEnd2PacketMessages, + clientICQMessages: pathEnd2ClientICQMessages, } pp.appendInitialMessageIfNecessary(messageLifecycle, &pathEnd1Messages, &pathEnd2Messages) @@ -739,6 +779,18 @@ func (pp *PathProcessor) assembleMessage( zap.String("port_id", m.info.PortID), ) } + case clientICQMessage: + message, err = pp.assembleClientICQMessage(ctx, m, src, dst) + om.clientICQMsgs[i] = clientICQMessageToTrack{ + msg: m, + assembled: err == nil, + } + if err == nil { + dst.log.Debug("Will send ICQ message", + zap.String("type", m.info.Type), + zap.String("query_id", string(m.info.QueryID)), + ) + } } if err != nil { pp.log.Error("Error assembling channel message", zap.Error(err)) @@ -753,7 +805,7 @@ func (pp *PathProcessor) assembleAndSendMessages( messages pathEndMessages, ) error { var needsClientUpdate bool - if len(messages.packetMessages) == 0 && len(messages.connectionMessages) == 0 && len(messages.channelMessages) == 0 { + if len(messages.packetMessages) == 0 && len(messages.connectionMessages) == 0 && len(messages.channelMessages) == 0 && len(messages.clientICQMessages) == 0 { var consensusHeightTime time.Time if dst.clientState.ConsensusTime.IsZero() { h, err := src.chainProvider.QueryIBCHeader(ctx, int64(dst.clientState.ConsensusHeight.RevisionHeight)) @@ -783,7 +835,7 @@ func (pp *PathProcessor) assembleAndSendMessages( msgs: make( []provider.RelayerMessage, 0, - len(messages.packetMessages)+len(messages.connectionMessages)+len(messages.channelMessages), + len(messages.packetMessages)+len(messages.connectionMessages)+len(messages.channelMessages)+len(messages.clientICQMessages), ), } msgUpdateClient, err := pp.assembleMsgUpdateClient(ctx, src, dst) @@ -816,6 +868,17 @@ func (pp *PathProcessor) assembleAndSendMessages( wg.Wait() } + if len(om.msgs) == 1 { + om.clientICQMsgs = make([]clientICQMessageToTrack, len(messages.clientICQMessages)) + // only assemble and send ICQ messages if there are no conn or chan handshake messages + for i, msg := range messages.clientICQMessages { + wg.Add(1) + go pp.assembleMessage(ctx, msg, src, dst, &om, i, &wg) + } + + wg.Wait() + } + if len(om.msgs) == 1 { om.pktMsgs = make([]packetMessageToTrack, len(messages.packetMessages)) // only assemble and send packet messages if there are no handshake messages @@ -840,6 +903,10 @@ func (pp *PathProcessor) assembleAndSendMessages( dst.trackProcessingChannelMessage(m) } + for _, m := range om.clientICQMsgs { + dst.trackProcessingClientICQMessage(m) + } + for _, m := range om.pktMsgs { dst.trackProcessingPacketMessage(m) } @@ -1019,6 +1086,22 @@ func (pp *PathProcessor) assembleChannelMessage( return assembleMessage(msg.info, proof) } +func (pp *PathProcessor) assembleClientICQMessage( + ctx context.Context, + msg clientICQMessage, + src, dst *pathEndRuntime, +) (provider.RelayerMessage, error) { + ctx, cancel := context.WithTimeout(ctx, interchainQueryTimeout) + defer cancel() + + proof, err := src.chainProvider.QueryICQWithProof(ctx, msg.info.Type, msg.info.Request, src.latestBlock.Height-1) + if err != nil { + return nil, fmt.Errorf("error during interchain query: %w", err) + } + + return dst.chainProvider.MsgSubmitQueryResponse(msg.info.Chain, msg.info.QueryID, proof) +} + func (pp *PathProcessor) channelMessagesToSend(pathEnd1ChannelHandshakeRes, pathEnd2ChannelHandshakeRes pathEndChannelHandshakeResponse) ([]channelIBCMessage, []channelIBCMessage) { pathEnd1ChannelSrcLen := len(pathEnd1ChannelHandshakeRes.SrcMessages) pathEnd1ChannelDstLen := len(pathEnd1ChannelHandshakeRes.DstMessages) diff --git a/relayer/processor/types.go b/relayer/processor/types.go index c411a101c..e4594dde7 100644 --- a/relayer/processor/types.go +++ b/relayer/processor/types.go @@ -75,6 +75,7 @@ type IBCMessagesCache struct { PacketFlow ChannelPacketMessagesCache ConnectionHandshake ConnectionMessagesCache ChannelHandshake ChannelMessagesCache + ClientICQ ClientICQMessagesCache } // Clone makes a deep copy of an IBCMessagesCache. @@ -83,10 +84,12 @@ func (c IBCMessagesCache) Clone() IBCMessagesCache { PacketFlow: make(ChannelPacketMessagesCache, len(c.PacketFlow)), ConnectionHandshake: make(ConnectionMessagesCache, len(c.ConnectionHandshake)), ChannelHandshake: make(ChannelMessagesCache, len(c.ChannelHandshake)), + ClientICQ: make(ClientICQMessagesCache, len(c.ClientICQ)), } x.PacketFlow.Merge(c.PacketFlow) x.ConnectionHandshake.Merge(c.ConnectionHandshake) x.ChannelHandshake.Merge(c.ChannelHandshake) + x.ClientICQ.Merge(c.ClientICQ) return x } @@ -96,6 +99,7 @@ func NewIBCMessagesCache() IBCMessagesCache { PacketFlow: make(ChannelPacketMessagesCache), ConnectionHandshake: make(ConnectionMessagesCache), ChannelHandshake: make(ChannelMessagesCache), + ClientICQ: make(ClientICQMessagesCache), } } @@ -120,6 +124,15 @@ type ConnectionMessagesCache map[string]ConnectionMessageCache // ConnectionMessageCache is used for caching connection handshake IBC messages for a given IBC connection. type ConnectionMessageCache map[ConnectionKey]provider.ConnectionInfo +// ClientICQType string wrapper for query/response type. +type ClientICQType string + +// ClientICQMessagesCache is used for caching a ClientICQMessageCache for a given type (query/response). +type ClientICQMessagesCache map[ClientICQType]ClientICQMessageCache + +// ClientICQMessageCache is used for caching a client ICQ message for a given query ID. +type ClientICQMessageCache map[provider.ClientICQQueryID]provider.ClientICQInfo + // ChannelKey is the key used for identifying channels between ChainProcessor and PathProcessor. type ChannelKey struct { ChannelID string @@ -399,6 +412,41 @@ func (c ChannelMessageCache) Merge(other ChannelMessageCache) { } } +// Retain creates cache path if it doesn't exist, then caches message. +func (c ClientICQMessagesCache) Retain(icqType ClientICQType, ci provider.ClientICQInfo) { + queryID := ci.QueryID + if _, ok := c[icqType]; !ok { + c[icqType] = make(ClientICQMessageCache) + } + c[icqType][queryID] = ci +} + +// Merge merges another ClientICQMessagesCache into this one. +func (c ClientICQMessagesCache) Merge(other ClientICQMessagesCache) { + for k, v := range other { + _, ok := c[k] + if !ok { + c[k] = v + } else { + c[k].Merge(v) + } + } +} + +// Merge merges another ClientICQMessageCache into this one. +func (c ClientICQMessageCache) Merge(other ClientICQMessageCache) { + for k, v := range other { + c[k] = v + } +} + +// DeleteMessages deletes cached messages for the provided query ID. +func (c ClientICQMessagesCache) DeleteMessages(queryID provider.ClientICQQueryID) { + for _, cm := range c { + delete(cm, queryID) + } +} + // IBCHeaderCache holds a mapping of IBCHeaders for their block height. type IBCHeaderCache map[uint64]provider.IBCHeader diff --git a/relayer/processor/types_internal.go b/relayer/processor/types_internal.go index f9a56c8f1..0582dad92 100644 --- a/relayer/processor/types_internal.go +++ b/relayer/processor/types_internal.go @@ -16,6 +16,7 @@ type pathEndMessages struct { connectionMessages []connectionIBCMessage channelMessages []channelIBCMessage packetMessages []packetIBCMessage + clientICQMessages []clientICQMessage } type ibcMessage interface { @@ -55,6 +56,19 @@ type connectionIBCMessage struct { func (connectionIBCMessage) ibcMessageIndicator() {} +const ( + ClientICQTypeRequest ClientICQType = "query_request" + ClientICQTypeResponse ClientICQType = "query_response" +) + +// clientICQMessage holds a client ICQ message info, +// useful for sending messages around internal to the PathProcessor. +type clientICQMessage struct { + info provider.ClientICQInfo +} + +func (clientICQMessage) ibcMessageIndicator() {} + // processingMessage tracks the state of a IBC message currently being processed. type processingMessage struct { assembled bool @@ -102,6 +116,8 @@ func (c connectionProcessingCache) deleteMessages(toDelete ...map[string][]Conne } } +type clientICQProcessingCache map[provider.ClientICQQueryID]processingMessage + // contains MsgRecvPacket from counterparty // entire packet flow type pathEndPacketFlowMessages struct { @@ -191,11 +207,12 @@ func channelInfoChannelKey(c provider.ChannelInfo) ChannelKey { // outgoingMessages is a slice of relayer messages that can be // appended to concurrently. type outgoingMessages struct { - mu sync.Mutex - msgs []provider.RelayerMessage - pktMsgs []packetMessageToTrack - connMsgs []connectionMessageToTrack - chanMsgs []channelMessageToTrack + mu sync.Mutex + msgs []provider.RelayerMessage + pktMsgs []packetMessageToTrack + connMsgs []connectionMessageToTrack + chanMsgs []channelMessageToTrack + clientICQMsgs []clientICQMessageToTrack } // MarshalLogObject satisfies the zapcore.ObjectMarshaler interface @@ -254,6 +271,11 @@ type channelMessageToTrack struct { assembled bool } +type clientICQMessageToTrack struct { + msg clientICQMessage + assembled bool +} + // orderFromString parses a string into a channel order byte. func orderFromString(order string) chantypes.Order { switch strings.ToUpper(order) { diff --git a/relayer/provider/provider.go b/relayer/provider/provider.go index 8a1a500d1..a8fa6efdf 100644 --- a/relayer/provider/provider.go +++ b/relayer/provider/provider.go @@ -13,6 +13,7 @@ import ( commitmenttypes "github.com/cosmos/ibc-go/v5/modules/core/23-commitment/types" ibcexported "github.com/cosmos/ibc-go/v5/modules/core/exported" "github.com/gogo/protobuf/proto" + "github.com/tendermint/tendermint/proto/tendermint/crypto" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -124,6 +125,21 @@ type ChannelInfo struct { Version string } +// ClientICQQueryID string wrapper for query ID. +type ClientICQQueryID string + +// ClientICQInfo contains relevant properties from client ICQ messages. +// Client ICQ implementation does not use IBC connections and channels. +type ClientICQInfo struct { + Source string + Connection string + Chain string + QueryID ClientICQQueryID + Type string + Request []byte + Height uint64 +} + // PacketProof includes all of the proof parameters needed for packet flows. type PacketProof struct { Proof []byte @@ -146,6 +162,12 @@ type ChannelProof struct { Version string } +type ICQProof struct { + Result []byte + ProofOps *crypto.ProofOps // TODO swap out tendermint type for third party chains. + Height int64 +} + // loggableEvents is an unexported wrapper type for a slice of RelayerEvent, // to satisfy the zapcore.ArrayMarshaler interface. type loggableEvents []RelayerEvent @@ -327,6 +349,17 @@ type ChainProvider interface { // [End] Client IBC message assembly + // [Begin] Client ICQ message assembly + + // QueryICQWithProof performs an ABCI query and includes the required proofOps. + QueryICQWithProof(ctx context.Context, msgType string, request []byte, height uint64) (ICQProof, error) + + // MsgSubmitQueryResponse takes the counterparty chain ID, the ICQ query ID, and the query result proof, + // then assembles a MsgSubmitQueryResponse message formatted for sending to this chain. + MsgSubmitQueryResponse(chainID string, queryID ClientICQQueryID, proof ICQProof) (RelayerMessage, error) + + // [End] Client ICQ message assembly + // Query heavy relay methods. Only used for flushing old packets. RelayPacketFromSequence(ctx context.Context, src ChainProvider, srch, dsth, seq uint64, srcChanID, srcPortID string, order chantypes.Order) (RelayerMessage, RelayerMessage, error) diff --git a/scripts/protocgen.sh b/scripts/protocgen.sh new file mode 100755 index 000000000..b2c7fe0ba --- /dev/null +++ b/scripts/protocgen.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -eo pipefail + +protoc_gen_gocosmos() { + if ! grep "github.com/gogo/protobuf => github.com/regen-network/protobuf" go.mod &>/dev/null ; then + echo -e "\tPlease run this command from somewhere inside the cosmos-sdk folder." + return 1 + fi + + go get github.com/regen-network/cosmos-proto/protoc-gen-gocosmos@latest 2>/dev/null +} + +protoc_gen_gocosmos + +proto_dirs=$(find ./proto -path -prune -o -name '*.proto' -print0 | xargs -0 -n1 dirname | sort | uniq) +for dir in $proto_dirs; do + buf protoc \ + -I "proto" \ + -I "third_party/proto" \ + --gocosmos_out=plugins=interfacetype+grpc:. \ + --grpc-gateway_out=logtostderr=true,allow_colon_final_segments=true:. \ + $(find "${dir}" -maxdepth 1 -name '*.proto') + +done + +# move proto files to the right places +# +# Note: Proto files are suffixed with the current binary version. +cp -r github.com/cosmos/relayer/* ./ +rm -rf github.com \ No newline at end of file diff --git a/third_party/proto/cosmos_proto/cosmos.proto b/third_party/proto/cosmos_proto/cosmos.proto new file mode 100644 index 000000000..35227bc66 --- /dev/null +++ b/third_party/proto/cosmos_proto/cosmos.proto @@ -0,0 +1,97 @@ +syntax = "proto3"; +package cosmos_proto; + +import "google/protobuf/descriptor.proto"; + +option go_package = "github.com/cosmos/cosmos-proto;cosmos_proto"; + +extend google.protobuf.MessageOptions { + + // implements_interface is used to indicate the type name of the interface + // that a message implements so that it can be used in google.protobuf.Any + // fields that accept that interface. A message can implement multiple + // interfaces. Interfaces should be declared using a declare_interface + // file option. + repeated string implements_interface = 93001; +} + +extend google.protobuf.FieldOptions { + + // accepts_interface is used to annotate that a google.protobuf.Any + // field accepts messages that implement the specified interface. + // Interfaces should be declared using a declare_interface file option. + string accepts_interface = 93001; + + // scalar is used to indicate that this field follows the formatting defined + // by the named scalar which should be declared with declare_scalar. Code + // generators may choose to use this information to map this field to a + // language-specific type representing the scalar. + string scalar = 93002; +} + +extend google.protobuf.FileOptions { + + // declare_interface declares an interface type to be used with + // accepts_interface and implements_interface. Interface names are + // expected to follow the following convention such that their declaration + // can be discovered by tools: for a given interface type a.b.C, it is + // expected that the declaration will be found in a protobuf file named + // a/b/interfaces.proto in the file descriptor set. + repeated InterfaceDescriptor declare_interface = 793021; + + // declare_scalar declares a scalar type to be used with + // the scalar field option. Scalar names are + // expected to follow the following convention such that their declaration + // can be discovered by tools: for a given scalar type a.b.C, it is + // expected that the declaration will be found in a protobuf file named + // a/b/scalars.proto in the file descriptor set. + repeated ScalarDescriptor declare_scalar = 793022; +} + +// InterfaceDescriptor describes an interface type to be used with +// accepts_interface and implements_interface and declared by declare_interface. +message InterfaceDescriptor { + + // name is the name of the interface. It should be a short-name (without + // a period) such that the fully qualified name of the interface will be + // package.name, ex. for the package a.b and interface named C, the + // fully-qualified name will be a.b.C. + string name = 1; + + // description is a human-readable description of the interface and its + // purpose. + string description = 2; +} + +// ScalarDescriptor describes an scalar type to be used with +// the scalar field option and declared by declare_scalar. +// Scalars extend simple protobuf built-in types with additional +// syntax and semantics, for instance to represent big integers. +// Scalars should ideally define an encoding such that there is only one +// valid syntactical representation for a given semantic meaning, +// i.e. the encoding should be deterministic. +message ScalarDescriptor { + + // name is the name of the scalar. It should be a short-name (without + // a period) such that the fully qualified name of the scalar will be + // package.name, ex. for the package a.b and scalar named C, the + // fully-qualified name will be a.b.C. + string name = 1; + + // description is a human-readable description of the scalar and its + // encoding format. For instance a big integer or decimal scalar should + // specify precisely the expected encoding format. + string description = 2; + + // field_type is the type of field with which this scalar can be used. + // Scalars can be used with one and only one type of field so that + // encoding standards and simple and clear. Currently only string and + // bytes fields are supported for scalars. + repeated ScalarType field_type = 3; +} + +enum ScalarType { + SCALAR_TYPE_UNSPECIFIED = 0; + SCALAR_TYPE_STRING = 1; + SCALAR_TYPE_BYTES = 2; +} \ No newline at end of file diff --git a/third_party/proto/gogoproto/gogo.proto b/third_party/proto/gogoproto/gogo.proto new file mode 100644 index 000000000..588291f0e --- /dev/null +++ b/third_party/proto/gogoproto/gogo.proto @@ -0,0 +1,145 @@ +// Protocol Buffers for Go with Gadgets +// +// Copyright (c) 2013, The GoGo Authors. All rights reserved. +// http://github.com/gogo/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto2"; +package gogoproto; + +import "google/protobuf/descriptor.proto"; + +option java_package = "com.google.protobuf"; +option java_outer_classname = "GoGoProtos"; +option go_package = "github.com/gogo/protobuf/gogoproto"; + +extend google.protobuf.EnumOptions { + optional bool goproto_enum_prefix = 62001; + optional bool goproto_enum_stringer = 62021; + optional bool enum_stringer = 62022; + optional string enum_customname = 62023; + optional bool enumdecl = 62024; +} + +extend google.protobuf.EnumValueOptions { + optional string enumvalue_customname = 66001; +} + +extend google.protobuf.FileOptions { + optional bool goproto_getters_all = 63001; + optional bool goproto_enum_prefix_all = 63002; + optional bool goproto_stringer_all = 63003; + optional bool verbose_equal_all = 63004; + optional bool face_all = 63005; + optional bool gostring_all = 63006; + optional bool populate_all = 63007; + optional bool stringer_all = 63008; + optional bool onlyone_all = 63009; + + optional bool equal_all = 63013; + optional bool description_all = 63014; + optional bool testgen_all = 63015; + optional bool benchgen_all = 63016; + optional bool marshaler_all = 63017; + optional bool unmarshaler_all = 63018; + optional bool stable_marshaler_all = 63019; + + optional bool sizer_all = 63020; + + optional bool goproto_enum_stringer_all = 63021; + optional bool enum_stringer_all = 63022; + + optional bool unsafe_marshaler_all = 63023; + optional bool unsafe_unmarshaler_all = 63024; + + optional bool goproto_extensions_map_all = 63025; + optional bool goproto_unrecognized_all = 63026; + optional bool gogoproto_import = 63027; + optional bool protosizer_all = 63028; + optional bool compare_all = 63029; + optional bool typedecl_all = 63030; + optional bool enumdecl_all = 63031; + + optional bool goproto_registration = 63032; + optional bool messagename_all = 63033; + + optional bool goproto_sizecache_all = 63034; + optional bool goproto_unkeyed_all = 63035; +} + +extend google.protobuf.MessageOptions { + optional bool goproto_getters = 64001; + optional bool goproto_stringer = 64003; + optional bool verbose_equal = 64004; + optional bool face = 64005; + optional bool gostring = 64006; + optional bool populate = 64007; + optional bool stringer = 67008; + optional bool onlyone = 64009; + + optional bool equal = 64013; + optional bool description = 64014; + optional bool testgen = 64015; + optional bool benchgen = 64016; + optional bool marshaler = 64017; + optional bool unmarshaler = 64018; + optional bool stable_marshaler = 64019; + + optional bool sizer = 64020; + + optional bool unsafe_marshaler = 64023; + optional bool unsafe_unmarshaler = 64024; + + optional bool goproto_extensions_map = 64025; + optional bool goproto_unrecognized = 64026; + + optional bool protosizer = 64028; + optional bool compare = 64029; + + optional bool typedecl = 64030; + + optional bool messagename = 64033; + + optional bool goproto_sizecache = 64034; + optional bool goproto_unkeyed = 64035; +} + +extend google.protobuf.FieldOptions { + optional bool nullable = 65001; + optional bool embed = 65002; + optional string customtype = 65003; + optional string customname = 65004; + optional string jsontag = 65005; + optional string moretags = 65006; + optional string casttype = 65007; + optional string castkey = 65008; + optional string castvalue = 65009; + + optional bool stdtime = 65010; + optional bool stdduration = 65011; + optional bool wktpointer = 65012; + + optional string castrepeated = 65013; +} \ No newline at end of file diff --git a/third_party/proto/tendermint/crypto/proof.proto b/third_party/proto/tendermint/crypto/proof.proto new file mode 100644 index 000000000..53c974f00 --- /dev/null +++ b/third_party/proto/tendermint/crypto/proof.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; +package tendermint.crypto; + +option go_package = "github.com/tendermint/tendermint/proto/tendermint/crypto"; + +import "gogoproto/gogo.proto"; + +message Proof { + int64 total = 1; + int64 index = 2; + bytes leaf_hash = 3; + repeated bytes aunts = 4; +} + +message ValueOp { + // Encoded in ProofOp.Key. + bytes key = 1; + + // To encode in ProofOp.Data + Proof proof = 2; +} + +message DominoOp { + string key = 1; + string input = 2; + string output = 3; +} + +// ProofOp defines an operation used for calculating Merkle root +// The data could be arbitrary format, providing nessecary data +// for example neighbouring node hash +message ProofOp { + string type = 1; + bytes key = 2; + bytes data = 3; +} + +// ProofOps is Merkle proof defined by the list of ProofOps +message ProofOps { + repeated ProofOp ops = 1 [(gogoproto.nullable) = false]; +} \ No newline at end of file