diff --git a/.github/compatibility-test-matrices/release-v6.1.x/client-chain-a.json b/.github/compatibility-test-matrices/release-v6.1.x/client-chain-a.json index 79bc3005ed1..aa86869814c 100644 --- a/.github/compatibility-test-matrices/release-v6.1.x/client-chain-a.json +++ b/.github/compatibility-test-matrices/release-v6.1.x/client-chain-a.json @@ -3,7 +3,8 @@ "chain-b": ["release-v6.1.x", "v6.1.0", "v5.2.0", "v4.3.0", "v4.2.0", "v4.1.1", "v3.4.0", "v3.3.1"], "entrypoint": ["TestClientTestSuite"], "test": [ - "TestClientUpdateProposal_Succeeds" + "TestClientUpdateProposal_Succeeds", + "TestClient_Update_Misbehaviour" ], "relayer-type": ["rly"], "chain-binary": ["simd"], diff --git a/.github/compatibility-test-matrices/release-v6.1.x/client-chain-b.json b/.github/compatibility-test-matrices/release-v6.1.x/client-chain-b.json index 53f70071854..d7dd9c32f0c 100644 --- a/.github/compatibility-test-matrices/release-v6.1.x/client-chain-b.json +++ b/.github/compatibility-test-matrices/release-v6.1.x/client-chain-b.json @@ -3,7 +3,8 @@ "chain-b": ["release-v6.1.x"], "entrypoint": ["TestClientTestSuite"], "test": [ - "TestClientUpdateProposal_Succeeds" + "TestClientUpdateProposal_Succeeds", + "TestClient_Update_Misbehaviour" ], "relayer-type": ["rly"], "chain-binary": ["simd"], diff --git a/.github/compatibility-test-matrices/release-v7.0.x/client-chain-a.json b/.github/compatibility-test-matrices/release-v7.0.x/client-chain-a.json index ce0b032b90b..9cd17bbbf12 100644 --- a/.github/compatibility-test-matrices/release-v7.0.x/client-chain-a.json +++ b/.github/compatibility-test-matrices/release-v7.0.x/client-chain-a.json @@ -3,7 +3,8 @@ "chain-b": ["release-v7.0.x", "v6.1.0", "v5.2.0", "v4.3.0", "v4.2.0", "v4.1.1", "v3.4.0", "v3.3.1"], "entrypoint": ["TestClientTestSuite"], "test": [ - "TestClientUpdateProposal_Succeeds" + "TestClientUpdateProposal_Succeeds", + "TestClient_Update_Misbehaviour" ], "relayer-type": ["rly"], "chain-binary": ["simd"], diff --git a/.github/compatibility-test-matrices/release-v7.0.x/client-chain-b.json b/.github/compatibility-test-matrices/release-v7.0.x/client-chain-b.json index 5c49fbc7b9c..452dca0061c 100644 --- a/.github/compatibility-test-matrices/release-v7.0.x/client-chain-b.json +++ b/.github/compatibility-test-matrices/release-v7.0.x/client-chain-b.json @@ -3,7 +3,8 @@ "chain-b": ["release-v7.0.x"], "entrypoint": ["TestClientTestSuite"], "test": [ - "TestClientUpdateProposal_Succeeds" + "TestClientUpdateProposal_Succeeds", + "TestClient_Update_Misbehaviour" ], "relayer-type": ["rly"], "chain-binary": ["simd"], diff --git a/.github/compatibility-test-matrices/release-v7.1.x/client-chain-a.json b/.github/compatibility-test-matrices/release-v7.1.x/client-chain-a.json index 42227d4e2ae..d149b4a035a 100644 --- a/.github/compatibility-test-matrices/release-v7.1.x/client-chain-a.json +++ b/.github/compatibility-test-matrices/release-v7.1.x/client-chain-a.json @@ -3,7 +3,8 @@ "chain-b": ["release-v7.1.x", "v7.0.0-rc1", "v6.1.0", "v5.2.0", "v4.3.0", "v4.2.0", "v4.1.1", "v3.4.0", "v3.3.1"], "entrypoint": ["TestClientTestSuite"], "test": [ - "TestClientUpdateProposal_Succeeds" + "TestClientUpdateProposal_Succeeds", + "TestClient_Update_Misbehaviour" ], "relayer-type": ["rly"], "chain-binary": ["simd"], diff --git a/.github/compatibility-test-matrices/release-v7.1.x/client-chain-b.json b/.github/compatibility-test-matrices/release-v7.1.x/client-chain-b.json index ca06a0f7f6c..9e093343acf 100644 --- a/.github/compatibility-test-matrices/release-v7.1.x/client-chain-b.json +++ b/.github/compatibility-test-matrices/release-v7.1.x/client-chain-b.json @@ -3,7 +3,8 @@ "chain-b": ["release-v7.1.x"], "entrypoint": ["TestClientTestSuite"], "test": [ - "TestClientUpdateProposal_Succeeds" + "TestClientUpdateProposal_Succeeds", + "TestClient_Update_Misbehaviour" ], "relayer-type": ["rly"], "chain-binary": ["simd"], diff --git a/e2e/tests/core/client_test.go b/e2e/tests/core/client_test.go index 03b1b9566cd..1f9cbb1cf4d 100644 --- a/e2e/tests/core/client_test.go +++ b/e2e/tests/core/client_test.go @@ -3,19 +3,38 @@ package e2e import ( "context" "fmt" + "sort" "strings" "testing" "time" + "github.com/cometbft/cometbft/crypto/tmhash" + tmjson "github.com/cometbft/cometbft/libs/json" + "github.com/cometbft/cometbft/privval" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + tmprotoversion "github.com/cometbft/cometbft/proto/tendermint/version" + tmtypes "github.com/cometbft/cometbft/types" + tmversion "github.com/cometbft/cometbft/version" + "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" + "github.com/strangelove-ventures/interchaintest/v7/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v7/ibc" test "github.com/strangelove-ventures/interchaintest/v7/testutil" "github.com/stretchr/testify/suite" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + + "github.com/cosmos/ibc-go/e2e/dockerutil" "github.com/cosmos/ibc-go/e2e/testsuite" "github.com/cosmos/ibc-go/e2e/testvalues" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" ibctesting "github.com/cosmos/ibc-go/v7/testing" + ibcmock "github.com/cosmos/ibc-go/v7/testing/mock" +) + +const ( + invalidHashValue = "invalid_hash" ) func TestClientTestSuite(t *testing.T) { @@ -119,3 +138,205 @@ func (s *ClientTestSuite) TestClientUpdateProposal_Succeeds() { }) }) } + +func (s *ClientTestSuite) TestClient_Update_Misbehaviour() { + t := s.T() + ctx := context.TODO() + + var ( + trustedHeight clienttypes.Height + latestHeight clienttypes.Height + clientState ibcexported.ClientState + block *tmproto.Block + signers []tmtypes.PrivValidator + validatorSet []*tmtypes.Validator + maliciousHeader *ibctm.Header + err error + ) + + relayer, _ := s.SetupChainsRelayerAndChannel(ctx) + chainA, chainB := s.GetChains() + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB)) + + t.Run("update clients", func(t *testing.T) { + err := relayer.UpdateClients(ctx, s.GetRelayerExecReporter(), s.GetPathName(0)) + s.Require().NoError(err) + + clientState, err = s.QueryClientState(ctx, chainA, ibctesting.FirstClientID) + s.Require().NoError(err) + }) + + t.Run("fetch trusted height", func(t *testing.T) { + tmClientState, ok := clientState.(*ibctm.ClientState) + s.Require().True(ok) + + trustedHeight, ok = tmClientState.GetLatestHeight().(clienttypes.Height) + s.Require().True(ok) + }) + + t.Run("update clients", func(t *testing.T) { + err := relayer.UpdateClients(ctx, s.GetRelayerExecReporter(), s.GetPathName(0)) + s.Require().NoError(err) + + clientState, err = s.QueryClientState(ctx, chainA, ibctesting.FirstClientID) + s.Require().NoError(err) + }) + + t.Run("fetch client state latest height", func(t *testing.T) { + tmClientState, ok := clientState.(*ibctm.ClientState) + s.Require().True(ok) + + latestHeight, ok = tmClientState.GetLatestHeight().(clienttypes.Height) + s.Require().True(ok) + }) + + t.Run("create validator set", func(t *testing.T) { + var validators []*tmservice.Validator + + t.Run("fetch block at latest client state height", func(t *testing.T) { + block, err = s.GetBlockByHeight(ctx, chainB, latestHeight.GetRevisionHeight()) + s.Require().NoError(err) + }) + + t.Run("get validators at latest height", func(t *testing.T) { + validators, err = s.GetValidatorSetByHeight(ctx, chainB, latestHeight.GetRevisionHeight()) + s.Require().NoError(err) + }) + + t.Run("extract validator private keys", func(t *testing.T) { + privateKeys := s.extractChainPrivateKeys(ctx, chainB) + for i, pv := range privateKeys { + pubKey, err := pv.GetPubKey() + s.Require().NoError(err) + + validator := tmtypes.NewValidator(pubKey, validators[i].VotingPower) + + validatorSet = append(validatorSet, validator) + signers = append(signers, pv) + } + }) + }) + + t.Run("create malicious header", func(t *testing.T) { + valSet := tmtypes.NewValidatorSet(validatorSet) + maliciousHeader, err = createMaliciousTMHeader(chainB.Config().ChainID, int64(latestHeight.GetRevisionHeight()), trustedHeight, + block.Header.GetTime(), valSet, valSet, signers, &block.Header) + s.Require().NoError(err) + }) + + t.Run("update client with duplicate misbehaviour header", func(t *testing.T) { + rlyWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + msgUpdateClient, err := clienttypes.NewMsgUpdateClient(ibctesting.FirstClientID, maliciousHeader, rlyWallet.FormattedAddress()) + s.Require().NoError(err) + + txResp, err := s.BroadcastMessages(ctx, chainA, rlyWallet, msgUpdateClient) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + }) + + t.Run("ensure client status is frozen", func(t *testing.T) { + status, err := s.QueryClientStatus(ctx, chainA, ibctesting.FirstClientID) + s.Require().NoError(err) + s.Require().Equal(ibcexported.Frozen.String(), status) + }) +} + +// extractChainPrivateKeys returns a slice of tmtypes.PrivValidator which hold the private keys for all validator +// nodes for a given chain. +func (s *ClientTestSuite) extractChainPrivateKeys(ctx context.Context, chain *cosmos.CosmosChain) []tmtypes.PrivValidator { + testContainers, err := dockerutil.GetTestContainers(s.T(), ctx, s.DockerClient) + s.Require().NoError(err) + + var filePvs []privval.FilePVKey + var pvs []tmtypes.PrivValidator + for _, container := range testContainers { + isNodeForDifferentChain := !strings.Contains(container.Names[0], chain.Config().ChainID) + isFullNode := strings.Contains(container.Names[0], fmt.Sprintf("%s-fn", chain.Config().ChainID)) + if isNodeForDifferentChain || isFullNode { + continue + } + + validatorPrivKey := fmt.Sprintf("/var/cosmos-chain/%s/config/priv_validator_key.json", chain.Config().Name) + privKeyFileContents, err := dockerutil.GetFileContentsFromContainer(ctx, s.DockerClient, container.ID, validatorPrivKey) + s.Require().NoError(err) + + var filePV privval.FilePVKey + err = tmjson.Unmarshal(privKeyFileContents, &filePV) + s.Require().NoError(err) + filePvs = append(filePvs, filePV) + } + + // We sort by address as GetValidatorSetByHeight also sorts by address. When iterating over them, the index + // will correspond to the correct ibcmock.PV. + sort.SliceStable(filePvs, func(i, j int) bool { + return filePvs[i].Address.String() < filePvs[j].Address.String() + }) + + for _, filePV := range filePvs { + pvs = append(pvs, &ibcmock.PV{ + PrivKey: &ed25519.PrivKey{Key: filePV.PrivKey.Bytes()}, + }) + } + + return pvs +} + +// createMaliciousTMHeader creates a header with the provided trusted height with an invalid app hash. +func createMaliciousTMHeader( + chainID string, + blockHeight int64, + trustedHeight clienttypes.Height, + timestamp time.Time, + tmValSet, tmTrustedVals *tmtypes.ValidatorSet, + signers []tmtypes.PrivValidator, + oldHeader *tmproto.Header, +) (*ibctm.Header, error) { + tmHeader := tmtypes.Header{ + Version: tmprotoversion.Consensus{Block: tmversion.BlockProtocol, App: 2}, + ChainID: chainID, + Height: blockHeight, + Time: timestamp, + LastBlockID: ibctesting.MakeBlockID(make([]byte, tmhash.Size), 10_000, make([]byte, tmhash.Size)), + LastCommitHash: oldHeader.LastCommitHash, + ValidatorsHash: tmValSet.Hash(), + NextValidatorsHash: tmValSet.Hash(), + DataHash: tmhash.Sum([]byte(invalidHashValue)), + ConsensusHash: tmhash.Sum([]byte(invalidHashValue)), + AppHash: tmhash.Sum([]byte(invalidHashValue)), + LastResultsHash: tmhash.Sum([]byte(invalidHashValue)), + EvidenceHash: tmhash.Sum([]byte(invalidHashValue)), + ProposerAddress: tmValSet.Proposer.Address, //nolint:staticcheck + } + + hhash := tmHeader.Hash() + blockID := ibctesting.MakeBlockID(hhash, 3, tmhash.Sum([]byte(invalidHashValue))) + voteSet := tmtypes.NewVoteSet(chainID, blockHeight, 1, tmproto.PrecommitType, tmValSet) + + commit, err := tmtypes.MakeCommit(blockID, blockHeight, 1, voteSet, signers, timestamp) + if err != nil { + return nil, err + } + + signedHeader := &tmproto.SignedHeader{ + Header: tmHeader.ToProto(), + Commit: commit.ToProto(), + } + + valSet, err := tmValSet.ToProto() + if err != nil { + return nil, err + } + + trustedVals, err := tmTrustedVals.ToProto() + if err != nil { + return nil, err + } + + return &ibctm.Header{ + SignedHeader: signedHeader, + ValidatorSet: valSet, + TrustedHeight: trustedHeight, + TrustedValidators: trustedVals, + }, nil +} diff --git a/e2e/testsuite/codec.go b/e2e/testsuite/codec.go index 267f08886a9..52ad55ac6d0 100644 --- a/e2e/testsuite/codec.go +++ b/e2e/testsuite/codec.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/cosmos/cosmos-sdk/codec" - sdkcodec "github.com/cosmos/cosmos-sdk/crypto/codec" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/cosmos/cosmos-sdk/x/authz" @@ -62,7 +62,7 @@ func codecAndEncodingConfig() (*codec.ProtoCodec, simappparams.EncodingConfig) { govv1beta1.RegisterInterfaces(cfg.InterfaceRegistry) govv1.RegisterInterfaces(cfg.InterfaceRegistry) authtypes.RegisterInterfaces(cfg.InterfaceRegistry) - sdkcodec.RegisterInterfaces(cfg.InterfaceRegistry) + cryptocodec.RegisterInterfaces(cfg.InterfaceRegistry) grouptypes.RegisterInterfaces(cfg.InterfaceRegistry) proposaltypes.RegisterInterfaces(cfg.InterfaceRegistry) authz.RegisterInterfaces(cfg.InterfaceRegistry) diff --git a/e2e/testsuite/grpc_query.go b/e2e/testsuite/grpc_query.go index a7449de94ec..7c5d7764dd5 100644 --- a/e2e/testsuite/grpc_query.go +++ b/e2e/testsuite/grpc_query.go @@ -2,12 +2,15 @@ package testsuite import ( "context" + "sort" + "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" govtypesbeta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" intertxtypes "github.com/cosmos/interchain-accounts/x/inter-tx/types" "github.com/strangelove-ventures/interchaintest/v7/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v7/ibc" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" controllertypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/controller/types" feetypes "github.com/cosmos/ibc-go/v7/modules/apps/29-fee/types" @@ -27,8 +30,9 @@ func (s *E2ETestSuite) QueryClientState(ctx context.Context, chain ibc.Chain, cl return nil, err } - clientState, err := clienttypes.UnpackClientState(res.ClientState) - if err != nil { + cfg := EncodingConfig() + var clientState ibcexported.ClientState + if err := cfg.InterfaceRegistry.UnpackAny(res.ClientState, &clientState); err != nil { return nil, err } @@ -171,3 +175,35 @@ func (s *E2ETestSuite) QueryProposalV1(ctx context.Context, chain ibc.Chain, pro return *res.Proposal, nil } + +// GetBlockByHeight fetches the block at a given height. Note: we are explicitly using the res.Block type which has been +// deprecated instead of res.SdkBlock to support backwards compatibility tests. +func (s *E2ETestSuite) GetBlockByHeight(ctx context.Context, chain ibc.Chain, height uint64) (*tmproto.Block, error) { + tmService := s.GetChainGRCPClients(chain).ConsensusServiceClient + res, err := tmService.GetBlockByHeight(ctx, &tmservice.GetBlockByHeightRequest{ + Height: int64(height), + }) + if err != nil { + return nil, err + } + + return res.Block, nil +} + +// GetValidatorSetByHeight returns the validators of the given chain at the specified height. The returned validators +// are sorted by address. +func (s *E2ETestSuite) GetValidatorSetByHeight(ctx context.Context, chain ibc.Chain, height uint64) ([]*tmservice.Validator, error) { + tmService := s.GetChainGRCPClients(chain).ConsensusServiceClient + res, err := tmService.GetValidatorSetByHeight(ctx, &tmservice.GetValidatorSetByHeightRequest{ + Height: int64(height), + }) + if err != nil { + return nil, err + } + + sort.SliceStable(res.Validators, func(i, j int) bool { + return res.Validators[i].Address < res.Validators[j].Address + }) + + return res.Validators, nil +} diff --git a/e2e/testsuite/testsuite.go b/e2e/testsuite/testsuite.go index 2b3de38b21b..4ed48f30b50 100644 --- a/e2e/testsuite/testsuite.go +++ b/e2e/testsuite/testsuite.go @@ -8,6 +8,7 @@ import ( "time" "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" "github.com/cosmos/cosmos-sdk/client/tx" sdk "github.com/cosmos/cosmos-sdk/types" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" @@ -65,7 +66,7 @@ type E2ETestSuite struct { startRelayerFn func(relayer ibc.Relayer) // pathNameIndex is the latest index to be used for generating paths - pathNameIndex uint64 + pathNameIndex int64 } // GRPCClients holds a reference to any GRPC clients that are needed by the tests. @@ -86,6 +87,8 @@ type GRPCClients struct { ParamsQueryClient paramsproposaltypes.QueryClient AuthQueryClient authtypes.QueryClient AuthZQueryClient authz.QueryClient + + ConsensusServiceClient tmservice.ServiceClient } // path is a pairing of two chains which will be used in a test. @@ -199,8 +202,15 @@ func (s *E2ETestSuite) SetupSingleChain(ctx context.Context) *cosmos.CosmosChain // generatePathName generates the path name using the test suites name func (s *E2ETestSuite) generatePathName() string { - pathName := fmt.Sprintf("%s-path-%d", s.T().Name(), s.pathNameIndex) + path := s.GetPathName(s.pathNameIndex) s.pathNameIndex++ + return path +} + +// GetPathName returns the name of a path at a specific index. This can be used in tests +// when the path name is required. +func (s *E2ETestSuite) GetPathName(idx int64) string { + pathName := fmt.Sprintf("%s-path-%d", s.T().Name(), idx) return strings.ReplaceAll(pathName, "/", "-") } @@ -211,6 +221,7 @@ func (s *E2ETestSuite) generatePath(ctx context.Context, relayer ibc.Relayer) st chainBID := chainB.Config().ChainID pathName := s.generatePathName() + err := relayer.GeneratePath(ctx, s.GetRelayerExecReporter(), chainAID, chainBID, pathName) s.Require().NoError(err) @@ -408,18 +419,18 @@ func (s *E2ETestSuite) InitGRPCClients(chain *cosmos.CosmosChain) { } s.grpcClients[chain.Config().ChainID] = GRPCClients{ - ClientQueryClient: clienttypes.NewQueryClient(grpcConn), - ConnectionQueryClient: connectiontypes.NewQueryClient(grpcConn), - ChannelQueryClient: channeltypes.NewQueryClient(grpcConn), - FeeQueryClient: feetypes.NewQueryClient(grpcConn), - ICAQueryClient: controllertypes.NewQueryClient(grpcConn), - InterTxQueryClient: intertxtypes.NewQueryClient(grpcConn), - GovQueryClient: govtypesv1beta1.NewQueryClient(grpcConn), - GovQueryClientV1: govtypesv1.NewQueryClient(grpcConn), - GroupsQueryClient: grouptypes.NewQueryClient(grpcConn), - ParamsQueryClient: paramsproposaltypes.NewQueryClient(grpcConn), - AuthQueryClient: authtypes.NewQueryClient(grpcConn), - AuthZQueryClient: authz.NewQueryClient(grpcConn), + ClientQueryClient: clienttypes.NewQueryClient(grpcConn), + ChannelQueryClient: channeltypes.NewQueryClient(grpcConn), + FeeQueryClient: feetypes.NewQueryClient(grpcConn), + ICAQueryClient: controllertypes.NewQueryClient(grpcConn), + InterTxQueryClient: intertxtypes.NewQueryClient(grpcConn), + GovQueryClient: govtypesv1beta1.NewQueryClient(grpcConn), + GovQueryClientV1: govtypesv1.NewQueryClient(grpcConn), + GroupsQueryClient: grouptypes.NewQueryClient(grpcConn), + ParamsQueryClient: paramsproposaltypes.NewQueryClient(grpcConn), + AuthQueryClient: authtypes.NewQueryClient(grpcConn), + AuthZQueryClient: authz.NewQueryClient(grpcConn), + ConsensusServiceClient: tmservice.NewServiceClient(grpcConn), } }