From 72d1ad2f6f936a0a83032fbf38a16feb412b6e04 Mon Sep 17 00:00:00 2001 From: Justin Tieri <37750742+jtieri@users.noreply.github.com> Date: Mon, 13 Mar 2023 20:34:33 -0500 Subject: [PATCH] feat: implement misbehaviour detection (#1121) * feat: implement misbehaviour detection Misbehaviour detection was missing in the relayer, so it's been re-implemented such that on every MsgUpdateClient we compare the proposed consensus state to the trusted consensus state from the counterparty at the same height. If there is a deviation between the two a MsgSubmitMisbehaviour will be composed and broadcasted so that the light client is frozen. * chore: handle review feedback * chore: remove unnecessary import + use global tm codec --- interchaintest/go.mod | 6 +- interchaintest/go.sum | 16 +- interchaintest/misbehaviour_test.go | 315 ++++++++++++++++++ relayer/chains/cosmos/account.go | 2 +- .../chains/cosmos/cosmos_chain_processor.go | 4 +- relayer/chains/cosmos/event_parser.go | 1 + relayer/chains/cosmos/grpc_query.go | 4 +- relayer/chains/cosmos/keys.go | 2 +- relayer/chains/cosmos/provider.go | 32 +- relayer/chains/cosmos/query.go | 47 +-- relayer/chains/cosmos/tx.go | 38 ++- relayer/misbehaviour.go | 78 ----- relayer/processor/path_end_runtime.go | 46 ++- relayer/provider/matcher.go | 98 ++++++ relayer/provider/provider.go | 49 ++- 15 files changed, 565 insertions(+), 173 deletions(-) create mode 100644 interchaintest/misbehaviour_test.go delete mode 100644 relayer/misbehaviour.go diff --git a/interchaintest/go.mod b/interchaintest/go.mod index c105c73ab..0f529bd46 100644 --- a/interchaintest/go.mod +++ b/interchaintest/go.mod @@ -3,6 +3,7 @@ module github.com/cosmos/relayer/v2/interchaintest go 1.19 require ( + cosmossdk.io/simapp v0.0.0-20230224204036-a6adb0821462 github.com/cometbft/cometbft v0.37.0 github.com/cosmos/cosmos-sdk v0.47.0-rc3 github.com/cosmos/ibc-go/v7 v7.0.0-rc1 @@ -10,7 +11,7 @@ require ( github.com/docker/docker v20.10.19+incompatible github.com/icza/dyno v0.0.0-20220812133438-f0b6f8a18845 github.com/moby/moby v20.10.22+incompatible - github.com/strangelove-ventures/interchaintest/v7 v7.0.0-20230309173441-1cbc09c52979 + github.com/strangelove-ventures/interchaintest/v7 v7.0.0-20230309210425-6f04be9aab19 github.com/stretchr/testify v1.8.2 go.uber.org/zap v1.24.0 golang.org/x/sync v0.1.0 @@ -53,6 +54,7 @@ require ( github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chzyer/readline v1.5.1 // indirect + github.com/cockroachdb/apd/v2 v2.0.2 // indirect github.com/coinbase/rosetta-sdk-go/types v1.0.0 // indirect github.com/cometbft/cometbft-db v0.7.0 // indirect github.com/confio/ics23/go v0.9.0 // indirect @@ -90,6 +92,7 @@ require ( github.com/go-kit/kit v0.12.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gofrs/flock v0.8.1 // indirect @@ -196,6 +199,7 @@ require ( github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect github.com/tendermint/go-amino v0.16.0 // indirect github.com/tidwall/btree v1.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/ulikunitz/xz v0.5.11 // indirect github.com/vedhavyas/go-subkey v1.0.3 // indirect diff --git a/interchaintest/go.sum b/interchaintest/go.sum index 5070cd48d..53804d403 100644 --- a/interchaintest/go.sum +++ b/interchaintest/go.sum @@ -199,6 +199,8 @@ cosmossdk.io/errors v1.0.0-beta.7 h1:gypHW76pTQGVnHKo6QBkb4yFOJjC+sUGRc5Al3Odj1w cosmossdk.io/errors v1.0.0-beta.7/go.mod h1:mz6FQMJRku4bY7aqS/Gwfcmr/ue91roMEKAmDUDpBfE= cosmossdk.io/math v1.0.0-beta.6.0.20230216172121-959ce49135e4 h1:/jnzJ9zFsL7qkV8LCQ1JH3dYHh2EsKZ3k8Mr6AqqiOA= cosmossdk.io/math v1.0.0-beta.6.0.20230216172121-959ce49135e4/go.mod h1:gUVtWwIzfSXqcOT+lBVz2jyjfua8DoBdzRsIyaUAT/8= +cosmossdk.io/simapp v0.0.0-20230224204036-a6adb0821462 h1:g8muUHnXL8vhld2Sjilyhb1UQObc+x9GVuDK43TYZns= +cosmossdk.io/simapp v0.0.0-20230224204036-a6adb0821462/go.mod h1:4Dd3NLoLYoN90kZ0uyHoTHzVVk9+J0v4HhZRBNTAq2c= cosmossdk.io/tools/rosetta v0.2.1 h1:ddOMatOH+pbxWbrGJKRAawdBkPYLfKXutK9IETnjYxw= cosmossdk.io/tools/rosetta v0.2.1/go.mod h1:Pqdc1FdvkNV3LcNIkYWt2RQY6IP1ge6YWZk8MhhO9Hw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -386,6 +388,7 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= +github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/apd/v3 v3.1.0 h1:MK3Ow7LH0W8zkd5GMKA1PvS9qG3bWFI95WaVNfyZJ/w= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= @@ -669,7 +672,8 @@ github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -1367,10 +1371,8 @@ github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jH github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/strangelove-ventures/go-subkey v1.0.7 h1:cOP/Lajg3uxV/tvspu0m6+0Cu+DJgygkEAbx/s+f35I= github.com/strangelove-ventures/go-subkey v1.0.7/go.mod h1:E34izOIEm+sZ1YmYawYRquqBQWeZBjVB4pF7bMuhc1c= -github.com/strangelove-ventures/interchaintest/v7 v7.0.0-20230309172329-6fab57aa06de h1:BWiVB+0lCualTDFSlAUC2AYs+sq2r29O558gahhIyAA= -github.com/strangelove-ventures/interchaintest/v7 v7.0.0-20230309172329-6fab57aa06de/go.mod h1:3wUe5Ik4S4AzxS6ygivmC+VY7qngf/VOSn9FmXS7Ao4= -github.com/strangelove-ventures/interchaintest/v7 v7.0.0-20230309173441-1cbc09c52979 h1:3G0ZRyzEBF/QNYQnhg2wkIop8n1W2DBb9lofGcrmYDk= -github.com/strangelove-ventures/interchaintest/v7 v7.0.0-20230309173441-1cbc09c52979/go.mod h1:3wUe5Ik4S4AzxS6ygivmC+VY7qngf/VOSn9FmXS7Ao4= +github.com/strangelove-ventures/interchaintest/v7 v7.0.0-20230309210425-6f04be9aab19 h1:myDhIC75y5Kycqke0Go3PeRnsXvGW6fynHDGds2dKFk= +github.com/strangelove-ventures/interchaintest/v7 v7.0.0-20230309210425-6f04be9aab19/go.mod h1:DTYkHkPDFjGE0jGLSG3elpgngb9fhaCHdmM0ERRd/T4= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= @@ -1408,8 +1410,10 @@ github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2l github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= -github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= +github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= +github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= +github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= diff --git a/interchaintest/misbehaviour_test.go b/interchaintest/misbehaviour_test.go new file mode 100644 index 000000000..6d10003e9 --- /dev/null +++ b/interchaintest/misbehaviour_test.go @@ -0,0 +1,315 @@ +package interchaintest_test + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "testing" + "time" + + simappparams "cosmossdk.io/simapp/params" + "github.com/cometbft/cometbft/crypto/tmhash" + cometproto "github.com/cometbft/cometbft/proto/tendermint/types" + cometprotoversion "github.com/cometbft/cometbft/proto/tendermint/version" + comettypes "github.com/cometbft/cometbft/types" + cometversion "github.com/cometbft/cometbft/version" + sdked25519 "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/std" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + ibctypes "github.com/cosmos/ibc-go/v7/modules/core/types" + ibccomettypes "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + ibctesting "github.com/cosmos/ibc-go/v7/testing" + ibcmocks "github.com/cosmos/ibc-go/v7/testing/mock" + "github.com/cosmos/ibc-go/v7/testing/simapp" + relayertest "github.com/cosmos/relayer/v2/interchaintest" + "github.com/strangelove-ventures/interchaintest/v7" + "github.com/strangelove-ventures/interchaintest/v7/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v7/ibc" + "github.com/strangelove-ventures/interchaintest/v7/testreporter" + "github.com/strangelove-ventures/interchaintest/v7/testutil" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestScenarioMisbehaviourDetection(t *testing.T) { + if testing.Short() { + t.Skip() + } + + t.Parallel() + + numVals := 1 + numFullNodes := 0 + cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{ + {Name: "gaia", Version: "v9.0.0-rc1", NumValidators: &numVals, NumFullNodes: &numFullNodes, ChainConfig: ibc.ChainConfig{ChainID: "chain-a", GasPrices: "0.0uatom"}}, + {Name: "gaia", Version: "v9.0.0-rc1", NumValidators: &numVals, NumFullNodes: &numFullNodes, ChainConfig: ibc.ChainConfig{ChainID: "chain-b", GasPrices: "0.0uatom"}}}, + ) + + chains, err := cf.Chains(t.Name()) + require.NoError(t, err) + chainA, chainB := chains[0].(*cosmos.CosmosChain), chains[1].(*cosmos.CosmosChain) + + ctx := context.Background() + client, network := interchaintest.DockerSetup(t) + + rf := relayertest.NewRelayerFactory(relayertest.RelayerConfig{InitialBlockHistory: 50}) + r := rf.Build(t, client, network) + + const pathChainAChainB = "chainA-chainB" + + ic := interchaintest.NewInterchain(). + AddChain(chainA). + AddChain(chainB). + AddRelayer(r, "relayer"). + AddLink(interchaintest.InterchainLink{ + Chain1: chainA, + Chain2: chainB, + Relayer: r, + Path: pathChainAChainB, + CreateClientOpts: ibc.CreateClientOptions{TrustingPeriod: "15m"}, + }) + + rep := testreporter.NewNopReporter() + eRep := rep.RelayerExecReporter(t) + + require.NoError(t, ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + SkipPathCreation: false, + })) + + t.Cleanup(func() { + _ = ic.Close() + }) + + // create a new user account and wait a few blocks for it to be created on chain + user := interchaintest.GetAndFundTestUsers(t, ctx, "user-1", 10_000_000, chainA)[0] + err = testutil.WaitForBlocks(ctx, 5, chainA) + + // Start the relayer + require.NoError(t, r.StartRelayer(ctx, eRep, pathChainAChainB)) + + t.Cleanup( + func() { + err := r.StopRelayer(ctx, eRep) + if err != nil { + panic(fmt.Errorf("an error occured while stopping the relayer: %s", err)) + } + }, + ) + + // query latest height on chainB + latestHeight, err := chainB.Height(ctx) + require.NoError(t, err) + + // query header at height on chainB + h := int64(latestHeight) + header, err := queryHeaderAtHeight(ctx, t, h, chainB) + require.NoError(t, err) + + // query tm client state on chainA + const clientID = "07-tendermint-0" + + cmd := []string{"ibc", "client", "state", clientID} + + stdout, stderr, err := chainA.Validators[0].ExecQuery(ctx, cmd...) + require.NoError(t, err) + require.Empty(t, stderr) + + queryResp := clienttypes.QueryClientStateResponse{} + err = defaultEncoding().Codec.UnmarshalJSON(stdout, &queryResp) + require.NoError(t, err) + + clientState, err := clienttypes.UnpackClientState(queryResp.ClientState) + require.NoError(t, err) + + // get latest height from prev client state above & create new height + 1 + height := clientState.GetLatestHeight().(clienttypes.Height) + newHeight := clienttypes.NewHeight(height.RevisionNumber, height.RevisionHeight+1) + + // create a validator for signing duplicate header + keyBz, err := chainB.Validators[0].ReadFile(ctx, "config/priv_validator_key.json") + require.NoError(t, err) + + pvk := cosmos.PrivValidatorKeyFile{} + err = json.Unmarshal(keyBz, &pvk) + require.NoError(t, err) + + decodedKeyBz, err := base64.StdEncoding.DecodeString(pvk.PrivKey.Value) + require.NoError(t, err) + + privKey := &sdked25519.PrivKey{ + Key: decodedKeyBz, + } + + privVal := ibcmocks.PV{PrivKey: privKey} + pubKey, err := privVal.GetPubKey() + require.NoError(t, err) + + val := comettypes.NewValidator(pubKey, header.ValidatorSet.Proposer.VotingPower) + valSet := comettypes.NewValidatorSet([]*comettypes.Validator{val}) + signers := []comettypes.PrivValidator{privVal} + + // create a duplicate header + newHeader := createTMClientHeader( + t, + chainB.Config().ChainID, + int64(newHeight.RevisionHeight), + height, + header.GetTime().Add(time.Minute), + valSet, + valSet, + signers, + header, + ) + + // attempt to update client with duplicate header + b := cosmos.NewBroadcaster(t, chainA) + + msg, err := clienttypes.NewMsgUpdateClient(clientID, newHeader, user.FormattedAddress()) + require.NoError(t, err) + + resp, err := cosmos.BroadcastTx(ctx, b, user, msg) + require.NoError(t, err) + assertTransactionIsValid(t, resp) + + // wait for inclusion in a block + err = testutil.WaitForBlocks(ctx, 5, chainA) + require.NoError(t, err) + + // query tm client state on chainA to assert it is now frozen + stdout, stderr, err = chainA.Validators[0].ExecQuery(ctx, cmd...) + require.NoError(t, err) + require.Empty(t, stderr) + + newQueryResp := clienttypes.QueryClientStateResponse{} + err = defaultEncoding().Codec.UnmarshalJSON(stdout, &newQueryResp) + require.NoError(t, err) + + newClientState, err := clienttypes.UnpackClientState(newQueryResp.ClientState) + require.NoError(t, err) + + tmClientState, ok := newClientState.(*ibccomettypes.ClientState) + require.True(t, ok) + require.NotEqual(t, uint64(0), tmClientState.FrozenHeight.RevisionHeight) +} + +func assertTransactionIsValid(t *testing.T, resp sdk.TxResponse) { + require.NotNil(t, resp) + require.NotEqual(t, 0, resp.GasUsed) + require.NotEqual(t, 0, resp.GasWanted) + require.Equal(t, uint32(0), resp.Code) + require.NotEmpty(t, resp.Data) + require.NotEmpty(t, resp.TxHash) + require.NotEmpty(t, resp.Events) +} + +func queryHeaderAtHeight(ctx context.Context, t *testing.T, height int64, chain *cosmos.CosmosChain) (*ibccomettypes.Header, error) { + var ( + page = 1 + perPage = 100000 + ) + + res, err := chain.Validators[0].Client.Commit(ctx, &height) + require.NoError(t, err) + + val, err := chain.Validators[0].Client.Validators(ctx, &height, &page, &perPage) + require.NoError(t, err) + + protoVal, err := comettypes.NewValidatorSet(val.Validators).ToProto() + require.NoError(t, err) + + return &ibccomettypes.Header{ + SignedHeader: res.SignedHeader.ToProto(), + ValidatorSet: protoVal, + }, nil +} + +func createTMClientHeader( + t *testing.T, + chainID string, + blockHeight int64, + trustedHeight clienttypes.Height, + timestamp time.Time, + tmValSet, tmTrustedVals *comettypes.ValidatorSet, + signers []comettypes.PrivValidator, + oldHeader *ibccomettypes.Header, +) *ibccomettypes.Header { + var ( + valSet *cometproto.ValidatorSet + trustedVals *cometproto.ValidatorSet + ) + require.NotNil(t, tmValSet) + + vsetHash := tmValSet.Hash() + + tmHeader := comettypes.Header{ + Version: cometprotoversion.Consensus{Block: cometversion.BlockProtocol, App: 2}, + ChainID: chainID, + Height: blockHeight, + Time: timestamp, + LastBlockID: ibctesting.MakeBlockID(make([]byte, tmhash.Size), 10_000, make([]byte, tmhash.Size)), + LastCommitHash: oldHeader.Header.LastCommitHash, + DataHash: tmhash.Sum([]byte("data_hash")), + ValidatorsHash: vsetHash, + NextValidatorsHash: vsetHash, + ConsensusHash: tmhash.Sum([]byte("consensus_hash")), + AppHash: tmhash.Sum([]byte("app_hash")), + LastResultsHash: tmhash.Sum([]byte("last_results_hash")), + EvidenceHash: tmhash.Sum([]byte("evidence_hash")), + ProposerAddress: tmValSet.Proposer.Address, + } + + hhash := tmHeader.Hash() + blockID := ibctesting.MakeBlockID(hhash, 3, tmhash.Sum([]byte("part_set"))) + voteSet := comettypes.NewVoteSet(chainID, blockHeight, 1, cometproto.PrecommitType, tmValSet) + + commit, err := comettypes.MakeCommit(blockID, blockHeight, 1, voteSet, signers, timestamp) + require.NoError(t, err) + + signedHeader := &cometproto.SignedHeader{ + Header: tmHeader.ToProto(), + Commit: commit.ToProto(), + } + + if tmValSet != nil { + valSet, err = tmValSet.ToProto() + if err != nil { + panic(err) + } + } + + if tmTrustedVals != nil { + trustedVals, err = tmTrustedVals.ToProto() + if err != nil { + panic(err) + } + } + + return &ibccomettypes.Header{ + SignedHeader: signedHeader, + ValidatorSet: valSet, + TrustedHeight: trustedHeight, + TrustedValidators: trustedVals, + } +} + +func defaultEncoding() simappparams.EncodingConfig { + cfg := simappparams.MakeTestEncodingConfig() + std.RegisterLegacyAminoCodec(cfg.Amino) + std.RegisterInterfaces(cfg.InterfaceRegistry) + simapp.ModuleBasics.RegisterLegacyAminoCodec(cfg.Amino) + simapp.ModuleBasics.RegisterInterfaces(cfg.InterfaceRegistry) + + banktypes.RegisterInterfaces(cfg.InterfaceRegistry) + ibctypes.RegisterInterfaces(cfg.InterfaceRegistry) + ibccomettypes.RegisterInterfaces(cfg.InterfaceRegistry) + transfertypes.RegisterInterfaces(cfg.InterfaceRegistry) + + return cfg +} diff --git a/relayer/chains/cosmos/account.go b/relayer/chains/cosmos/account.go index 0c95afd9c..a3b656352 100644 --- a/relayer/chains/cosmos/account.go +++ b/relayer/chains/cosmos/account.go @@ -49,7 +49,7 @@ func (cc *CosmosProvider) GetAccountWithHeight(clientCtx client.Context, addr sd } var acc authtypes.AccountI - if err := cc.Codec.InterfaceRegistry.UnpackAny(res.Account, &acc); err != nil { + if err := cc.Cdc.InterfaceRegistry.UnpackAny(res.Account, &acc); err != nil { return nil, 0, err } diff --git a/relayer/chains/cosmos/cosmos_chain_processor.go b/relayer/chains/cosmos/cosmos_chain_processor.go index c39bfa4a7..bf594165b 100644 --- a/relayer/chains/cosmos/cosmos_chain_processor.go +++ b/relayer/chains/cosmos/cosmos_chain_processor.go @@ -348,7 +348,7 @@ func (ccp *CosmosChainProcessor) queryCycle(ctx context.Context, persistence *qu ppChanged := false - var latestHeader CosmosIBCHeader + var latestHeader provider.TendermintIBCHeader newLatestQueriedBlock := persistence.latestQueriedBlock @@ -377,7 +377,7 @@ func (ccp *CosmosChainProcessor) queryCycle(ctx context.Context, persistence *qu break } - latestHeader = ibcHeader.(CosmosIBCHeader) + latestHeader = ibcHeader.(provider.TendermintIBCHeader) heightUint64 := uint64(i) diff --git a/relayer/chains/cosmos/event_parser.go b/relayer/chains/cosmos/event_parser.go index 78d2b8e66..38ef47288 100644 --- a/relayer/chains/cosmos/event_parser.go +++ b/relayer/chains/cosmos/event_parser.go @@ -175,6 +175,7 @@ func (c clientInfo) ClientState(trustingPeriod time.Duration) provider.ClientSta ClientID: c.clientID, ConsensusHeight: c.consensusHeight, TrustingPeriod: trustingPeriod, + Header: c.header, } } diff --git a/relayer/chains/cosmos/grpc_query.go b/relayer/chains/cosmos/grpc_query.go index 32658a337..dec0c84a8 100644 --- a/relayer/chains/cosmos/grpc_query.go +++ b/relayer/chains/cosmos/grpc_query.go @@ -78,8 +78,8 @@ func (cc *CosmosProvider) Invoke(ctx context.Context, method string, req, reply *header.HeaderAddr = outMd } - if cc.Codec.InterfaceRegistry != nil { - return types.UnpackInterfaces(reply, cc.Codec.Marshaler) + if cc.Cdc.InterfaceRegistry != nil { + return types.UnpackInterfaces(reply, cc.Cdc.Marshaler) } return nil diff --git a/relayer/chains/cosmos/keys.go b/relayer/chains/cosmos/keys.go index 7e0c4d77b..272c96b30 100644 --- a/relayer/chains/cosmos/keys.go +++ b/relayer/chains/cosmos/keys.go @@ -38,7 +38,7 @@ func KeyringAlgoOptions() keyring.Option { // CreateKeystore initializes a new instance of a keyring at the specified path in the local filesystem. func (cc *CosmosProvider) CreateKeystore(path string) error { - keybase, err := keyring.New(cc.PCfg.ChainID, cc.PCfg.KeyringBackend, cc.PCfg.KeyDirectory, cc.Input, cc.Codec.Marshaler, KeyringAlgoOptions()) + keybase, err := keyring.New(cc.PCfg.ChainID, cc.PCfg.KeyringBackend, cc.PCfg.KeyDirectory, cc.Input, cc.Cdc.Marshaler, KeyringAlgoOptions()) if err != nil { return err } diff --git a/relayer/chains/cosmos/provider.go b/relayer/chains/cosmos/provider.go index 21fb11212..b56bcdc61 100644 --- a/relayer/chains/cosmos/provider.go +++ b/relayer/chains/cosmos/provider.go @@ -14,14 +14,11 @@ import ( rpcclient "github.com/cometbft/cometbft/rpc/client" rpchttp "github.com/cometbft/cometbft/rpc/client/http" libclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" - tmtypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/gogoproto/proto" commitmenttypes "github.com/cosmos/ibc-go/v7/modules/core/23-commitment/types" - ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" - tmclient "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" "github.com/cosmos/relayer/v2/relayer/codecs/ethermint" "github.com/cosmos/relayer/v2/relayer/processor" "github.com/cosmos/relayer/v2/relayer/provider" @@ -93,7 +90,7 @@ func (pc CosmosProviderConfig) NewProvider(log *zap.Logger, homepath string, deb Output: os.Stdout, // TODO: this is a bit of a hack, we should probably have a better way to inject modules - Codec: MakeCodec(pc.Modules, pc.ExtraCodecs), + Cdc: MakeCodec(pc.Modules, pc.ExtraCodecs), } return cp, nil @@ -109,7 +106,7 @@ type CosmosProvider struct { LightProvider provtypes.Provider Input io.Reader Output io.Writer - Codec Codec + Cdc Codec // TODO: GRPC Client type? nextAccountSeq uint64 @@ -125,27 +122,6 @@ type CosmosProvider struct { cometLegacyEncoding bool } -type CosmosIBCHeader struct { - SignedHeader *tmtypes.SignedHeader - ValidatorSet *tmtypes.ValidatorSet -} - -func (h CosmosIBCHeader) Height() uint64 { - return uint64(h.SignedHeader.Height) -} - -func (h CosmosIBCHeader) ConsensusState() ibcexported.ConsensusState { - return &tmclient.ConsensusState{ - Timestamp: h.SignedHeader.Time, - Root: commitmenttypes.NewMerkleRoot(h.SignedHeader.AppHash), - NextValidatorsHash: h.SignedHeader.NextValidatorsHash, - } -} - -func (h CosmosIBCHeader) NextValidatorsHash() []byte { - return h.SignedHeader.NextValidatorsHash -} - func (cc *CosmosProvider) ProviderConfig() provider.ProviderConfig { return cc.PCfg } @@ -230,7 +206,7 @@ func (cc *CosmosProvider) TrustingPeriod(ctx context.Context) (time.Duration, er // Sprint returns the json representation of the specified proto message. func (cc *CosmosProvider) Sprint(toPrint proto.Message) (string, error) { - out, err := cc.Codec.Marshaler.MarshalJSON(toPrint) + out, err := cc.Cdc.Marshaler.MarshalJSON(toPrint) if err != nil { return "", err } @@ -241,7 +217,7 @@ func (cc *CosmosProvider) Sprint(toPrint proto.Message) (string, error) { // Once initialization is complete an attempt to query the underlying node's tendermint version is performed. // NOTE: Init must be called after creating a new instance of CosmosProvider. func (cc *CosmosProvider) Init(ctx context.Context) error { - keybase, err := keyring.New(cc.PCfg.ChainID, cc.PCfg.KeyringBackend, cc.PCfg.KeyDirectory, cc.Input, cc.Codec.Marshaler, cc.KeyringOptions...) + keybase, err := keyring.New(cc.PCfg.ChainID, cc.PCfg.KeyringBackend, cc.PCfg.KeyDirectory, cc.Input, cc.Cdc.Marshaler, cc.KeyringOptions...) if err != nil { return err } diff --git a/relayer/chains/cosmos/query.go b/relayer/chains/cosmos/query.go index dcc0f455a..c4690f6b3 100644 --- a/relayer/chains/cosmos/query.go +++ b/relayer/chains/cosmos/query.go @@ -262,7 +262,7 @@ func (cc *CosmosProvider) QueryTendermintProof(ctx context.Context, height int64 return nil, nil, clienttypes.Height{}, err } - cdc := codec.NewProtoCodec(cc.Codec.InterfaceRegistry) + cdc := codec.NewProtoCodec(cc.Cdc.InterfaceRegistry) proofBz, err := cdc.Marshal(&merkleProof) if err != nil { @@ -287,7 +287,7 @@ func (cc *CosmosProvider) QueryClientStateResponse(ctx context.Context, height i return nil, sdkerrors.Wrap(clienttypes.ErrClientNotFound, srcClientId) } - cdc := codec.NewProtoCodec(cc.Codec.InterfaceRegistry) + cdc := codec.NewProtoCodec(cc.Cdc.InterfaceRegistry) clientState, err := clienttypes.UnmarshalClientState(cdc, value) if err != nil { @@ -336,7 +336,7 @@ func (cc *CosmosProvider) QueryClientConsensusState(ctx context.Context, chainHe return nil, sdkerrors.Wrap(clienttypes.ErrConsensusStateNotFound, clientid) } - cdc := codec.NewProtoCodec(cc.Codec.InterfaceRegistry) + cdc := codec.NewProtoCodec(cc.Cdc.InterfaceRegistry) cs, err := clienttypes.UnmarshalConsensusState(cdc, value) if err != nil { @@ -373,7 +373,7 @@ func (cc *CosmosProvider) QueryUpgradeProof(ctx context.Context, key []byte, hei return nil, clienttypes.Height{}, err } - proof, err := cc.Codec.Marshaler.Marshal(&merkleProof) + proof, err := cc.Cdc.Marshaler.Marshal(&merkleProof) if err != nil { return nil, clienttypes.Height{}, err } @@ -533,7 +533,7 @@ func (cc *CosmosProvider) queryConnectionABCI(ctx context.Context, height int64, return nil, sdkerrors.Wrap(conntypes.ErrConnectionNotFound, connectionID) } - cdc := codec.NewProtoCodec(cc.Codec.InterfaceRegistry) + cdc := codec.NewProtoCodec(cc.Cdc.InterfaceRegistry) var connection conntypes.ConnectionEnd if err := cdc.Unmarshal(value, &connection); err != nil { @@ -679,7 +679,7 @@ func (cc *CosmosProvider) queryChannelABCI(ctx context.Context, height int64, po return nil, sdkerrors.Wrapf(chantypes.ErrChannelNotFound, "portID (%s), channelID (%s)", portID, channelID) } - cdc := codec.NewProtoCodec(cc.Codec.InterfaceRegistry) + cdc := codec.NewProtoCodec(cc.Cdc.InterfaceRegistry) var channel chantypes.Channel if err := cdc.Unmarshal(value, &channel); err != nil { @@ -1037,39 +1037,6 @@ func (cc *CosmosProvider) QueryStatus(ctx context.Context) (*coretypes.ResultSta return status, nil } -// QueryHeaderAtHeight returns the header at a given height -func (cc *CosmosProvider) QueryHeaderAtHeight(ctx context.Context, height int64) (ibcexported.ClientMessage, error) { - var ( - page = 1 - perPage = 100000 - ) - if height <= 0 { - return nil, fmt.Errorf("must pass in valid height, %d not valid", height) - } - - res, err := cc.RPCClient.Commit(ctx, &height) - if err != nil { - return nil, err - } - - val, err := cc.RPCClient.Validators(ctx, &height, &page, &perPage) - if err != nil { - return nil, err - } - - protoVal, err := tmtypes.NewValidatorSet(val.Validators).ToProto() - if err != nil { - return nil, err - } - - return &tmclient.Header{ - // NOTE: This is not a SignedHeader - // We are missing a light.Commit type here - SignedHeader: res.SignedHeader.ToProto(), - ValidatorSet: protoVal, - }, nil -} - // QueryDenomTrace takes a denom from IBC and queries the information about it func (cc *CosmosProvider) QueryDenomTrace(ctx context.Context, denom string) (*transfertypes.DenomTrace, error) { transfers, err := transfertypes.NewQueryClient(cc).DenomTrace(ctx, @@ -1140,7 +1107,7 @@ func (cc *CosmosProvider) QueryConsensusStateABCI(ctx context.Context, clientID } // TODO do we really want to create a new codec? ChainClient exposes proto.Marshaler - cdc := codec.NewProtoCodec(cc.Codec.InterfaceRegistry) + cdc := codec.NewProtoCodec(cc.Cdc.InterfaceRegistry) cs, err := clienttypes.UnmarshalConsensusState(cdc, value) if err != nil { diff --git a/relayer/chains/cosmos/tx.go b/relayer/chains/cosmos/tx.go index a20fb0cab..f62c11602 100644 --- a/relayer/chains/cosmos/tx.go +++ b/relayer/chains/cosmos/tx.go @@ -300,7 +300,7 @@ func (cc *CosmosProvider) waitForBlockInclusion( // mkTxResult decodes a comet transaction into an SDK TxResponse. func (cc *CosmosProvider) mkTxResult(resTx *coretypes.ResultTx) (*sdk.TxResponse, error) { - txbz, err := cc.Codec.TxConfig.TxDecoder()(resTx.Tx) + txbz, err := cc.Cdc.TxConfig.TxDecoder()(resTx.Tx) if err != nil { return nil, err } @@ -396,7 +396,7 @@ func (cc *CosmosProvider) buildMessages(ctx context.Context, msgs []provider.Rel // Generate the transaction bytes if err := retry.Do(func() error { var err error - txBytes, err = cc.Codec.TxConfig.TxEncoder()(tx) + txBytes, err = cc.Cdc.TxConfig.TxEncoder()(tx) if err != nil { return err } @@ -961,14 +961,14 @@ func (cc *CosmosProvider) MsgChannelCloseConfirm(msgCloseInit provider.ChannelIn } func (cc *CosmosProvider) MsgUpdateClientHeader(latestHeader provider.IBCHeader, trustedHeight clienttypes.Height, trustedHeader provider.IBCHeader) (ibcexported.ClientMessage, error) { - trustedCosmosHeader, ok := trustedHeader.(CosmosIBCHeader) + trustedCosmosHeader, ok := trustedHeader.(provider.TendermintIBCHeader) if !ok { - return nil, fmt.Errorf("unsupported IBC trusted header type, expected: CosmosIBCHeader, actual: %T", trustedHeader) + return nil, fmt.Errorf("unsupported IBC trusted header type, expected: TendermintIBCHeader, actual: %T", trustedHeader) } - latestCosmosHeader, ok := latestHeader.(CosmosIBCHeader) + latestCosmosHeader, ok := latestHeader.(provider.TendermintIBCHeader) if !ok { - return nil, fmt.Errorf("unsupported IBC header type, expected: CosmosIBCHeader, actual: %T", latestHeader) + return nil, fmt.Errorf("unsupported IBC header type, expected: TendermintIBCHeader, actual: %T", latestHeader) } trustedValidatorsProto, err := trustedCosmosHeader.ValidatorSet.ToProto() @@ -1028,6 +1028,20 @@ func (cc *CosmosProvider) MsgSubmitQueryResponse(chainID string, queryID provide return NewCosmosMessage(msg), nil } +func (cc *CosmosProvider) MsgSubmitMisbehaviour(clientID string, misbehaviour ibcexported.ClientMessage) (provider.RelayerMessage, error) { + signer, err := cc.Address() + if err != nil { + return nil, err + } + + msg, err := clienttypes.NewMsgSubmitMisbehaviour(clientID, misbehaviour, signer) + if err != nil { + return nil, err + } + + 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, @@ -1114,7 +1128,7 @@ func (cc *CosmosProvider) AcknowledgementFromSequence(ctx context.Context, dst p return msg, nil } -// QueryIBCHeader returns the IBC compatible block header (CosmosIBCHeader) at a specific height. +// QueryIBCHeader returns the IBC compatible block header (TendermintIBCHeader) at a specific height. func (cc *CosmosProvider) QueryIBCHeader(ctx context.Context, h int64) (provider.IBCHeader, error) { if h == 0 { return nil, fmt.Errorf("height cannot be 0") @@ -1125,7 +1139,7 @@ func (cc *CosmosProvider) QueryIBCHeader(ctx context.Context, h int64) (provider return nil, err } - return CosmosIBCHeader{ + return provider.TendermintIBCHeader{ SignedHeader: lightBlock.SignedHeader, ValidatorSet: lightBlock.ValidatorSet, }, nil @@ -1167,7 +1181,7 @@ func (cc *CosmosProvider) InjectTrustedFields(ctx context.Context, header ibcexp return err } - trustedValidators = ibcHeader.(CosmosIBCHeader).ValidatorSet + trustedValidators = ibcHeader.(provider.TendermintIBCHeader).ValidatorSet return err }, retry.Context(ctx), rtyAtt, rtyDel, rtyErr); err != nil { return nil, fmt.Errorf( @@ -1278,9 +1292,9 @@ func (cc *CosmosProvider) PrepareFactory(txf tx.Factory) (tx.Factory, error) { } cliCtx := client.Context{}.WithClient(cc.RPCClient). - WithInterfaceRegistry(cc.Codec.InterfaceRegistry). + WithInterfaceRegistry(cc.Cdc.InterfaceRegistry). WithChainID(cc.PCfg.ChainID). - WithCodec(cc.Codec.Marshaler) + WithCodec(cc.Cdc.Marshaler) // Set the account number and sequence on the transaction factory and retry if fail if err = retry.Do(func() error { @@ -1370,7 +1384,7 @@ func (cc *CosmosProvider) TxFactory() tx.Factory { return tx.Factory{}. WithAccountRetriever(cc). WithChainID(cc.PCfg.ChainID). - WithTxConfig(cc.Codec.TxConfig). + WithTxConfig(cc.Cdc.TxConfig). WithGasAdjustment(cc.PCfg.GasAdjustment). WithGasPrices(cc.PCfg.GasPrices). WithKeybase(cc.Keybase). diff --git a/relayer/misbehaviour.go b/relayer/misbehaviour.go deleted file mode 100644 index fcee1dfef..000000000 --- a/relayer/misbehaviour.go +++ /dev/null @@ -1,78 +0,0 @@ -package relayer - -//var ( -// // strings for parsing events -// updateCliTag = "update_client" -// headerTag = "header" -// clientIDTag = "client_id" -//) - -// checkAndSubmitMisbehaviour check headers from update_client tx events -// against the associated light client. If the headers do not match, the emitted -// header and a reconstructed header are used in misbehaviour submission to -// the IBC client on the source chain. -//func checkAndSubmitMisbehaviour(src, counterparty *Chain, events map[string][]string) error { -// hdrs, ok := events[fmt.Sprintf("%s.%s", updateCliTag, headerTag)] -// if !ok { -// return nil -// } -// for i, hdr := range hdrs { -// clientIDs := events[fmt.Sprintf("%s.%s", updateCliTag, clientIDTag)] -// if len(clientIDs) <= i { -// return fmt.Errorf("emitted client-ids count is less than emitted headers count") -// } -// -// emittedClientID := clientIDs[i] -// if src.PathEnd.ClientID != emittedClientID { -// continue -// } -// -// hdrBytes, err := hex.DecodeString(hdr) -// if err != nil { -// return sdkerrors.Wrapf(err, "failed decoding hexadecimal string of header with client-id: %s", -// emittedClientID) -// } -// -// exportedHeader, err := clienttypes.UnmarshalHeader(src.Encoding.Marshaler, hdrBytes) -// if err != nil { -// return sdkerrors.Wrapf(err, "failed unmarshaling header with client-id: %s", emittedClientID) -// } -// -// emittedHeader, ok := exportedHeader.(*tmclient.Header) -// if !ok { -// return fmt.Errorf("emitted header is not tendermint type") -// } -// -// trustedHeader, err := counterparty.ChainProvider.GetLightSignedHeaderAtHeight(emittedHeader.Header.Height) -// if err != nil { -// return err -// } -// -// if IsMatchingConsensusState(emittedHeader.ConsensusState(), trustedHeader.ConsensusState()) { -// continue -// } -// -// trustedHeader.TrustedValidators = emittedHeader.TrustedValidators -// trustedHeader.TrustedHeight = emittedHeader.TrustedHeight -// -// misbehaviour := tmclient.NewMisbehaviour(emittedClientID, emittedHeader, trustedHeader) -// msg, err := clienttypes.NewMsgSubmitMisbehaviour(emittedClientID, misbehaviour, src.MustGetAddress()) -// if err != nil { -// return err -// } -// if err := msg.ValidateBasic(); err != nil { -// return err -// } -// res, success, err := src.SendMsg(msg) -// if err != nil { -// return err -// } -// if !success { -// return fmt.Errorf("submit misbehaviour tx failed: %s", res.RawLog) -// } -// src.Log(fmt.Sprintf("Submitted misbehaviour for emitted header with height: %d", -// emittedHeader.Header.Height)) -// } -// -// return nil -//} diff --git a/relayer/processor/path_end_runtime.go b/relayer/processor/path_end_runtime.go index ed7b77aca..05c0f07b5 100644 --- a/relayer/processor/path_end_runtime.go +++ b/relayer/processor/path_end_runtime.go @@ -290,6 +290,40 @@ func (pathEnd *pathEndRuntime) shouldTerminate(ibcMessagesCache IBCMessagesCache return false } +// checkForMisbehaviour is called for each attempt to update the light client on this path end. The proposed header will +// be compared against the cached trusted header for the same block height to determine if there is a deviation in the +// consensus states, if there is no cached trusted header then it will be queried from the counterparty further down in +// the call stack. If a deviation is found a MsgSubmitMisbehaviour will be composed and broadcasted to freeze the +// light client. If misbehaviour is detected true will be returned and the pathEndRuntime should terminate. +// If no misbehaviour is detected false will be returned along with a nil error. +func (pathEnd *pathEndRuntime) checkForMisbehaviour( + ctx context.Context, + state provider.ClientState, + counterparty *pathEndRuntime, +) (bool, error) { + cachedHeader := counterparty.ibcHeaderCache[state.ConsensusHeight.RevisionHeight] + + misbehaviour, err := provider.CheckForMisbehaviour(ctx, counterparty.chainProvider, pathEnd.info.ClientID, state.Header, cachedHeader) + if err != nil { + return false, err + } + if misbehaviour == nil && err == nil { + return false, nil + } + + msgMisbehaviour, err := pathEnd.chainProvider.MsgSubmitMisbehaviour(pathEnd.info.ClientID, misbehaviour) + if err != nil { + return true, err + } + + _, _, err = pathEnd.chainProvider.SendMessage(ctx, msgMisbehaviour, "") + if err != nil { + return true, err + } + + return true, nil +} + func (pathEnd *pathEndRuntime) mergeCacheData(ctx context.Context, cancel func(), d ChainProcessorCacheData, counterpartyChainID string, counterpartyInSync bool, messageLifecycle MessageLifecycle, counterParty *pathEndRuntime) { pathEnd.lastClientUpdateHeightMu.Lock() pathEnd.latestBlock = d.LatestBlock @@ -298,6 +332,16 @@ func (pathEnd *pathEndRuntime) mergeCacheData(ctx context.Context, cancel func() pathEnd.inSync = d.InSync pathEnd.latestHeader = d.LatestHeader pathEnd.clientState = d.ClientState + + terminate, err := pathEnd.checkForMisbehaviour(ctx, pathEnd.clientState, counterParty) + if err != nil { + pathEnd.log.Error( + "Failed to check for misbehaviour", + zap.String("client_id", pathEnd.info.ClientID), + zap.Error(err), + ) + } + if d.ClientState.ConsensusHeight != pathEnd.clientState.ConsensusHeight { pathEnd.clientState = d.ClientState ibcHeader, ok := counterParty.ibcHeaderCache[d.ClientState.ConsensusHeight.RevisionHeight] @@ -308,7 +352,7 @@ func (pathEnd *pathEndRuntime) mergeCacheData(ctx context.Context, cancel func() pathEnd.handleCallbacks(d.IBCMessagesCache) - if pathEnd.shouldTerminate(d.IBCMessagesCache, messageLifecycle) { + if pathEnd.shouldTerminate(d.IBCMessagesCache, messageLifecycle) || terminate { cancel() return } diff --git a/relayer/provider/matcher.go b/relayer/provider/matcher.go index 8048ce118..72088b0d3 100644 --- a/relayer/provider/matcher.go +++ b/relayer/provider/matcher.go @@ -6,11 +6,21 @@ import ( "reflect" "time" + sdkcodec "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" tmclient "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" ) +var tendermintClientCodec = tmClientCodec() + +func tmClientCodec() *sdkcodec.ProtoCodec { + interfaceRegistry := types.NewInterfaceRegistry() + tmclient.RegisterInterfaces(interfaceRegistry) + return sdkcodec.NewProtoCodec(interfaceRegistry) +} + // ClientsMatch will check the type of an existing light client on the src chain, tracking the dst chain, and run // an appropriate matcher function to determine if the existing client's state matches a proposed new client // state constructed from the dst chain. @@ -33,6 +43,40 @@ func ClientsMatch(ctx context.Context, src, dst ChainProvider, existingClient cl return "", nil } +// CheckForMisbehaviour checks that a proposed header, for updating a light client, contains a consensus state that matches +// the trusted consensus state from the counterparty for the same block height. If the consensus states for the proposed +// header and the trusted header match then both returned values will be nil. +func CheckForMisbehaviour( + ctx context.Context, + counterparty ChainProvider, + clientID string, + proposedHeader []byte, + cachedHeader IBCHeader, +) (ibcexported.ClientMessage, error) { + var ( + misbehavior ibcexported.ClientMessage + err error + ) + + clientMsg, err := clienttypes.UnmarshalClientMessage(tendermintClientCodec, proposedHeader) + if err != nil { + return nil, err + } + + switch header := clientMsg.(type) { + case *tmclient.Header: + misbehavior, err = checkTendermintMisbehaviour(ctx, clientID, header, cachedHeader, counterparty) + if err != nil { + return nil, err + } + if misbehavior == nil && err == nil { + return nil, nil + } + } + + return misbehavior, nil +} + // cometMatcher determines if there is an existing light client on the src chain, tracking the dst chain, // with a state which matches a proposed new client state constructed from the dst chain. func cometMatcher(ctx context.Context, src, dst ChainProvider, existingClientID string, existingClient, newClient ibcexported.ClientState) (string, error) { @@ -118,3 +162,57 @@ func isMatchingTendermintClient(a, b tmclient.ClientState) bool { func isMatchingTendermintConsensusState(a, b *tmclient.ConsensusState) bool { return reflect.DeepEqual(*a, *b) } + +// checkTendermintMisbehaviour checks that a proposed consensus state, used to update a tendermint light client, +// matches the trusted consensus state from the counterparty chain. If there is no cached trusted header then +// it will be queried from the counterparty. If the consensus states for the proposed header and the trusted header +// match then both returned values will be nil. +func checkTendermintMisbehaviour( + ctx context.Context, + clientID string, + proposedHeader *tmclient.Header, + cachedHeader IBCHeader, + counterparty ChainProvider, +) (ibcexported.ClientMessage, error) { + var ( + trustedHeader *tmclient.Header + err error + ) + + if cachedHeader == nil { + header, err := counterparty.QueryIBCHeader(ctx, proposedHeader.Header.Height) + if err != nil { + return nil, err + } + + tmHeader, ok := header.(TendermintIBCHeader) + if !ok { + return nil, fmt.Errorf("failed to check for misbehaviour, expected %T, got %T", (*TendermintIBCHeader)(nil), header) + } + + trustedHeader, err = tmHeader.TMHeader() + if err != nil { + return nil, err + } + } else { + trustedHeader, err = cachedHeader.(TendermintIBCHeader).TMHeader() + if err != nil { + return nil, err + } + } + + if isMatchingTendermintConsensusState(proposedHeader.ConsensusState(), trustedHeader.ConsensusState()) { + return nil, nil + } + + // When we queried the light block in QueryIBCHeader we did not have the TrustedHeight or TrustedValidators, + // it is the relayer's responsibility to inject these trusted fields i.e. we need a height < the proposed headers height. + // The TrustedHeight is the height of a stored ConsensusState on the client that will be used to verify the new untrusted header. + // The Trusted ConsensusState must be within the unbonding period of current time in order to correctly verify, + // and the TrustedValidators must hash to TrustedConsensusState.NextValidatorsHash since that is the last trusted + // validator set at the TrustedHeight. + trustedHeader.TrustedValidators = proposedHeader.TrustedValidators + trustedHeader.TrustedHeight = proposedHeader.TrustedHeight + + return tmclient.NewMisbehaviour(clientID, proposedHeader, trustedHeader), nil +} diff --git a/relayer/provider/provider.go b/relayer/provider/provider.go index 996d8f989..2431604af 100644 --- a/relayer/provider/provider.go +++ b/relayer/provider/provider.go @@ -6,6 +6,7 @@ import ( "time" "github.com/cometbft/cometbft/proto/tendermint/crypto" + "github.com/cometbft/cometbft/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/gogoproto/proto" transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" @@ -14,6 +15,7 @@ import ( chantypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" commitmenttypes "github.com/cosmos/ibc-go/v7/modules/core/23-commitment/types" ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" + "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -67,6 +69,7 @@ type ClientState struct { ConsensusHeight clienttypes.Height TrustingPeriod time.Duration ConsensusTime time.Time + Header []byte } // ClientTrustedState holds the current state of a client from the perspective of both involved chains, @@ -233,7 +236,9 @@ type ChainProvider interface { MsgCreateClient(clientState ibcexported.ClientState, consensusState ibcexported.ConsensusState) (RelayerMessage, error) MsgUpgradeClient(srcClientId string, consRes *clienttypes.QueryConsensusStateResponse, clientRes *clienttypes.QueryClientStateResponse) (RelayerMessage, error) - // MsgSubmitMisbehavior(/*TODO*/) + + MsgSubmitMisbehaviour(clientID string, misbehaviour ibcexported.ClientMessage) (RelayerMessage, error) + // [End] Client IBC message assembly functions // ValidatePacket makes sure packet is valid to be relayed. @@ -514,3 +519,45 @@ func (t *TimeoutOnCloseError) Error() string { func NewTimeoutOnCloseError(msg string) *TimeoutOnCloseError { return &TimeoutOnCloseError{msg} } + +type TendermintIBCHeader struct { + SignedHeader *types.SignedHeader + ValidatorSet *types.ValidatorSet + TrustedValidators *types.ValidatorSet + TrustedHeight clienttypes.Height +} + +func (h TendermintIBCHeader) Height() uint64 { + return uint64(h.SignedHeader.Height) +} + +func (h TendermintIBCHeader) ConsensusState() ibcexported.ConsensusState { + return &tendermint.ConsensusState{ + Timestamp: h.SignedHeader.Time, + Root: commitmenttypes.NewMerkleRoot(h.SignedHeader.AppHash), + NextValidatorsHash: h.SignedHeader.NextValidatorsHash, + } +} + +func (h TendermintIBCHeader) NextValidatorsHash() []byte { + return h.SignedHeader.NextValidatorsHash +} + +func (h TendermintIBCHeader) TMHeader() (*tendermint.Header, error) { + valSet, err := h.ValidatorSet.ToProto() + if err != nil { + return nil, err + } + + trustedVals, err := h.TrustedValidators.ToProto() + if err != nil { + return nil, err + } + + return &tendermint.Header{ + SignedHeader: h.SignedHeader.ToProto(), + ValidatorSet: valSet, + TrustedHeight: h.TrustedHeight, + TrustedValidators: trustedVals, + }, nil +}