diff --git a/.gitignore b/.gitignore index 30346ae64..9ecaf916d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ chain-code/ two-chains/ibc-* two-chains/.relayer two-chains/*.log +test/setup/valkeys/*.json diff --git a/relayer/misbehaviour.go b/relayer/misbehaviour.go index 2f8cd6a11..db5c47e00 100644 --- a/relayer/misbehaviour.go +++ b/relayer/misbehaviour.go @@ -20,7 +20,7 @@ var ( // 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 *Chain, events map[string][]string) error { +func checkAndSubmitMisbehaviour(src, counterparty *Chain, events map[string][]string) error { hdrs, ok := events[fmt.Sprintf("%s.%s", updateCliTag, headerTag)] if !ok { return nil @@ -52,7 +52,7 @@ func checkAndSubmitMisbehaviour(src *Chain, events map[string][]string) error { return fmt.Errorf("emitted header is not tendermint type") } - trustedHeader, err := src.GetLightSignedHeaderAtHeight(emittedHeader.Header.Height) + trustedHeader, err := counterparty.GetLightSignedHeaderAtHeight(emittedHeader.Header.Height) if err != nil { return err } diff --git a/relayer/naive-strategy.go b/relayer/naive-strategy.go index ddc1ca9dd..696c3e6c9 100644 --- a/relayer/naive-strategy.go +++ b/relayer/naive-strategy.go @@ -232,7 +232,9 @@ func (nrs *NaiveStrategy) UnrelayedAcknowledgements(src, dst *Chain) (*RelaySequ // HandleEvents defines how the relayer will handle block and transaction events as they are emitted func (nrs *NaiveStrategy) HandleEvents(src, dst *Chain, events map[string][]string) { // check for misbehaviour and submit if found - err := checkAndSubmitMisbehaviour(src, events) + // events came from dst chain, use that chain as the source + // the chain messages are submitted to + err := checkAndSubmitMisbehaviour(dst, src, events) if err != nil { src.Error(err) } diff --git a/test/relayer_akash_test.go b/test/relayer_akash_test.go index 2ddd22359..25eb15f49 100644 --- a/test/relayer_akash_test.go +++ b/test/relayer_akash_test.go @@ -10,8 +10,8 @@ import ( var ( akashChains = []testChain{ - {"ibc-0", gaiaTestConfig}, - {"ibc-1", akashTestConfig}, + {"ibc-0", 0, gaiaTestConfig}, + {"ibc-1", 1, akashTestConfig}, } ) diff --git a/test/relayer_gaia_test.go b/test/relayer_gaia_test.go index 10d565ba7..c63eca44b 100644 --- a/test/relayer_gaia_test.go +++ b/test/relayer_gaia_test.go @@ -2,16 +2,26 @@ package test import ( "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/core/02-client/types" + ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/light-clients/07-tendermint/types" + ibctesting "github.com/cosmos/cosmos-sdk/x/ibc/testing" + ibctestingmock "github.com/cosmos/cosmos-sdk/x/ibc/testing/mock" "github.com/cosmos/relayer/relayer" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/tmhash" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmprotoversion "github.com/tendermint/tendermint/proto/tendermint/version" + tmtypes "github.com/tendermint/tendermint/types" + tmversion "github.com/tendermint/tendermint/version" ) var ( gaiaChains = []testChain{ - {"ibc-0", gaiaTestConfig}, - {"ibc-1", gaiaTestConfig}, + {"ibc-0", 0, gaiaTestConfig}, + {"ibc-1", 1, gaiaTestConfig}, } ) @@ -148,3 +158,164 @@ func TestGaiaReuseIdentifiers(t *testing.T) { require.Equal(t, expectedSrc, src) require.Equal(t, expectedDst, dst) } + +func TestGaiaMisbehaviourMonitoring(t *testing.T) { + chains := spinUpTestChains(t, gaiaChains...) + + var ( + src = chains.MustGet("ibc-0") + dst = chains.MustGet("ibc-1") + ) + + path, err := genTestPathAndSet(src, dst, "transfer", "transfer") + require.NoError(t, err) + + // create path + _, err = src.CreateClients(dst) + require.NoError(t, err) + testClientPair(t, src, dst) + + _, err = src.CreateOpenConnections(dst, 3, src.GetTimeout()) + require.NoError(t, err) + testConnectionPair(t, src, dst) + + _, err = src.CreateOpenChannels(dst, 3, src.GetTimeout()) + require.NoError(t, err) + testChannelPair(t, src, dst) + + // start the relayer process in it's own goroutine + rlyDone, err := relayer.RunStrategy(src, dst, path.MustGetStrategy()) + require.NoError(t, err) + + // Wait for relay message inclusion in both chains + require.NoError(t, src.WaitForNBlocks(1)) + require.NoError(t, dst.WaitForNBlocks(1)) + + latestHeight, err := dst.QueryLatestHeight() + require.NoError(t, err) + + header, err := dst.QueryHeaderAtHeight(latestHeight) + require.NoError(t, err) + + clientStateRes, err := src.QueryClientState(latestHeight) + require.NoError(t, err) + + // unpack any into ibc tendermint client state + clientStateExported, err := clienttypes.UnpackClientState(clientStateRes.ClientState) + require.NoError(t, err) + + // cast from interface to concrete type + clientState, ok := clientStateExported.(*ibctmtypes.ClientState) + require.True(t, ok, "error when casting exported clientstate") + + height := clientState.GetLatestHeight().(clienttypes.Height) + heightPlus1 := clienttypes.NewHeight(height.RevisionNumber, height.RevisionHeight+1) + + // setup validator for signing duplicate header + // use key for dst + privKey := getSDKPrivKey(1) + privVal := ibctestingmock.PV{ + PrivKey: privKey, + } + pubKey, err := privVal.GetPubKey() + require.NoError(t, err) + validator := tmtypes.NewValidator(pubKey, header.ValidatorSet.Proposer.VotingPower) + valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{validator}) + signers := []tmtypes.PrivValidator{privVal} + + // creating duplicate header + newHeader := createTMClientHeader(t, dst.ChainID, int64(heightPlus1.RevisionHeight), height, + header.GetTime().Add(time.Minute), valSet, valSet, signers, header) + + // update client with duplicate header + updateMsg, err := clienttypes.NewMsgUpdateClient(src.PathEnd.ClientID, newHeader, src.MustGetAddress()) + require.NoError(t, err) + + res, success, err := src.SendMsg(updateMsg) + require.NoError(t, err) + require.True(t, success) + require.Equal(t, uint32(0), res.Code) + + // wait for packet processing + require.NoError(t, dst.WaitForNBlocks(6)) + + // kill relayer routine + rlyDone() + + clientStateRes, err = src.QueryClientState(0) + require.NoError(t, err) + + // unpack any into ibc tendermint client state + clientStateExported, err = clienttypes.UnpackClientState(clientStateRes.ClientState) + require.NoError(t, err) + + // cast from interface to concrete type + clientState, ok = clientStateExported.(*ibctmtypes.ClientState) + require.True(t, ok, "error when casting exported clientstate") + + // clientstate should be frozen + require.True(t, clientState.IsFrozen()) +} + +func createTMClientHeader(t *testing.T, chainID string, blockHeight int64, trustedHeight clienttypes.Height, + timestamp time.Time, tmValSet, tmTrustedVals *tmtypes.ValidatorSet, signers []tmtypes.PrivValidator, + oldHeader *ibctmtypes.Header) *ibctmtypes.Header { + var ( + valSet *tmproto.ValidatorSet + trustedVals *tmproto.ValidatorSet + ) + require.NotNil(t, tmValSet) + + vsetHash := tmValSet.Hash() + + 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.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, //nolint:staticcheck + } + hhash := tmHeader.Hash() + blockID := ibctesting.MakeBlockID(hhash, 3, tmhash.Sum([]byte("part_set"))) + voteSet := tmtypes.NewVoteSet(chainID, blockHeight, 1, tmproto.PrecommitType, tmValSet) + + commit, err := tmtypes.MakeCommit(blockID, blockHeight, 1, voteSet, signers, timestamp) + require.NoError(t, err) + + signedHeader := &tmproto.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) + } + } + + // The trusted fields may be nil. They may be filled before relaying messages to a client. + // The relayer is responsible for querying client and injecting appropriate trusted fields. + return &ibctmtypes.Header{ + SignedHeader: signedHeader, + ValidatorSet: valSet, + TrustedHeight: trustedHeight, + TrustedValidators: trustedVals, + } +} diff --git a/test/setup/Dockerfile.akashtest b/test/setup/Dockerfile.akashtest index 9ee7666fd..a7de73861 100644 --- a/test/setup/Dockerfile.akashtest +++ b/test/setup/Dockerfile.akashtest @@ -8,7 +8,9 @@ USER root COPY ./akash-setup.sh . +COPY ./valkeys ./setup/valkeys + EXPOSE 26657 ENTRYPOINT [ "./akash-setup.sh" ] -# NOTE: to run this image, docker run -d -p 26657:26657 ./single-node.sh {{chain_id}} {{genesis_account}} +# NOTE: to run this image, docker run -d -p 26657:26657 ./akash-setup.sh {{chain_id}} {{genesis_account}} {{seeds}} {{priv_validator_key_path}} diff --git a/test/setup/Dockerfile.gaiatest b/test/setup/Dockerfile.gaiatest index 34bef1cb1..05df8d437 100644 --- a/test/setup/Dockerfile.gaiatest +++ b/test/setup/Dockerfile.gaiatest @@ -14,7 +14,15 @@ WORKDIR /gaia COPY ./gaia-setup.sh . +COPY ./valkeys ./setup/valkeys + +USER root + +RUN chmod -R 777 ./setup + +USER gaia + EXPOSE 26657 ENTRYPOINT [ "./gaia-setup.sh" ] -# NOTE: to run this image, docker run -d -p 26657:26657 ./single-node.sh {{chain_id}} {{genesis_account}} +# NOTE: to run this image, docker run -d -p 26657:26657 ./gaia-setup.sh {{chain_id}} {{genesis_account}} {{seeds}} {{priv_validator_key_path}} diff --git a/test/setup/akash-setup.sh b/test/setup/akash-setup.sh index 741575922..a7b9001a3 100755 --- a/test/setup/akash-setup.sh +++ b/test/setup/akash-setup.sh @@ -4,6 +4,7 @@ set -o errexit -o nounset CHAINID=$1 GENACCT=$2 +PRIVPATH=$3 if [ -z "$1" ]; then echo "Need to input chain id..." @@ -15,12 +16,18 @@ if [ -z "$2" ]; then exit 1 fi +if [ -z "$3" ]; then + echo "Need to input path of priv_validator_key json file" + exit 1 +fi + # Build genesis file incl account for passed address coins="10000000000stake,100000000000samoleans" akash init --chain-id $CHAINID $CHAINID akash keys add validator --keyring-backend="test" akash add-genesis-account $(akash keys show validator -a --keyring-backend="test") $coins akash add-genesis-account $GENACCT $coins +cp $PRIVPATH ~/.akash/config/priv_validator_key.json akash gentx validator 5000000000stake --keyring-backend="test" --chain-id $CHAINID akash collect-gentxs diff --git a/test/setup/gaia-setup.sh b/test/setup/gaia-setup.sh index ea9e4c689..76117e815 100755 --- a/test/setup/gaia-setup.sh +++ b/test/setup/gaia-setup.sh @@ -4,6 +4,7 @@ set -o errexit -o nounset CHAINID=$1 GENACCT=$2 +PRIVPATH=$3 if [ -z "$1" ]; then echo "Need to input chain id..." @@ -15,12 +16,18 @@ if [ -z "$2" ]; then exit 1 fi +if [ -z "$3" ]; then + echo "Need to input path of priv_validator_key json file" + exit 1 +fi + # Build genesis file incl account for passed address coins="10000000000stake,100000000000samoleans" gaiad init --chain-id $CHAINID $CHAINID gaiad keys add validator --keyring-backend="test" gaiad add-genesis-account $(gaiad keys show validator -a --keyring-backend="test") $coins gaiad add-genesis-account $GENACCT $coins +cp $PRIVPATH ~/.gaia/config/priv_validator_key.json gaiad gentx validator 5000000000stake --keyring-backend="test" --chain-id $CHAINID gaiad collect-gentxs diff --git a/test/setup/valkeys/.gitkeep b/test/setup/valkeys/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/test/test_chains.go b/test/test_chains.go index ec12d2609..4b2f29efb 100644 --- a/test/test_chains.go +++ b/test/test_chains.go @@ -11,6 +11,15 @@ import ( ry "github.com/cosmos/relayer/relayer" ) +const ( + // SEED1 is a mnenomic + //nolint:lll + SEED1 = "cake blossom buzz suspect image view round utility meat muffin humble club model latin similar glow draw useless kiwi snow laugh gossip roof public" + // SEED2 is a mnemonic + //nolint:lll + SEED2 = "near little movie lady moon fuel abandon gasp click element muscle elbow taste indoor soft soccer like occur legend coin near random normal adapt" +) + var ( // GAIA BLOCK TIMEOUTS are located in the gaia setup script in the // setup directory. @@ -37,6 +46,8 @@ var ( accountPrefix: "akash", trustingPeriod: "330h", } + + seeds = []string{SEED1, SEED2} ) type ( @@ -44,6 +55,7 @@ type ( // cosmos-sdk based blockchain testChain struct { chainID string + seed int t testChainConfig } diff --git a/test/test_setup.go b/test/test_setup.go index 2ad86f60f..63362aa3e 100644 --- a/test/test_setup.go +++ b/test/test_setup.go @@ -14,6 +14,11 @@ import ( "github.com/stretchr/testify/require" ry "github.com/cosmos/relayer/relayer" + + sdked25519 "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + sdkcryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + tmed25519 "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/privval" ) // spinUpTestChains is to be passed any number of test chains with given configuration options @@ -46,6 +51,7 @@ func spinUpTestChains(t *testing.T, testChains ...testChain) ry.Chains { c := newTestChain(t, tc) chains[i] = c wg.Add(1) + genPrivValKeyJSON(tc.seed) go spinUpTestContainer(t, rchan, pool, c, dir, &wg, tc) } @@ -135,7 +141,11 @@ func spinUpTestContainer(t *testing.T, rchan chan<- *dockertest.Resource, Repository: containerName, // Name must match Repository Tag: "latest", // Must match docker default build tag ExposedPorts: []string{tc.t.rpcPort, c.GetRPCPort()}, - Cmd: []string{c.ChainID, c.MustGetAddress().String()}, + Cmd: []string{ + c.ChainID, + c.MustGetAddress().String(), + getPrivValFileName(tc.seed), + }, PortBindings: map[dc.Port][]dc.PortBinding{ dc.Port(tc.t.rpcPort): {{HostPort: c.GetRPCPort()}}, }, @@ -207,3 +217,25 @@ func genTestPathAndSet(src, dst *ry.Chain, srcPort, dstPort string) (*ry.Path, e dst.PathEnd = path.Dst return path, nil } + +func genPrivValKeyJSON(seedNumber int) { + privKey := getPrivKey(seedNumber) + filePV := getFilePV(privKey, seedNumber) + filePV.Key.Save() +} + +func getPrivKey(seedNumber int) tmed25519.PrivKey { + return tmed25519.GenPrivKeyFromSecret([]byte(seeds[seedNumber])) +} + +func getSDKPrivKey(seedNumber int) sdkcryptotypes.PrivKey { + return sdked25519.GenPrivKeyFromSecret([]byte(seeds[seedNumber])) +} + +func getFilePV(privKey tmed25519.PrivKey, seedNumber int) *privval.FilePV { + return privval.NewFilePV(privKey, getPrivValFileName(seedNumber), "/") +} + +func getPrivValFileName(seedNumber int) string { + return fmt.Sprintf("./setup/valkeys/priv_val%d.json", seedNumber) +}