diff --git a/Dockerfile b/Dockerfile index 4d81392316..03939617be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ RUN go mod tidy RUN make install # Get Hermes build -FROM ghcr.io/informalsystems/hermes:1.4.1 AS hermes-builder +FROM otacrew/hermes-ics:latest AS hermes-builder # Get CometMock FROM informalofftermatt/cometmock:latest as cometmock-builder diff --git a/go.mod b/go.mod index 7ef3e43ffb..58b8eaa667 100644 --- a/go.mod +++ b/go.mod @@ -80,7 +80,6 @@ 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.5.1 // indirect - github.com/gobwas/ws v1.1.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/gateway v1.1.0 // indirect github.com/golang/glog v1.0.0 // indirect @@ -116,7 +115,7 @@ require ( github.com/minio/highwayhash v1.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mtibben/percent v0.2.1 // indirect - github.com/onsi/ginkgo v1.16.5 // indirect + github.com/onsi/ginkgo v1.16.4 // indirect github.com/onsi/gomega v1.20.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect diff --git a/go.sum b/go.sum index 5afd840ee9..ed38bf1bab 100644 --- a/go.sum +++ b/go.sum @@ -382,15 +382,12 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= -github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= @@ -780,8 +777,8 @@ github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= @@ -1268,7 +1265,6 @@ golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/tests/e2e/actions.go b/tests/e2e/actions.go index 06bcb7f5f7..9c7d41af17 100644 --- a/tests/e2e/actions.go +++ b/tests/e2e/actions.go @@ -63,7 +63,7 @@ type StartChainAction struct { validators []StartChainValidator // Genesis changes specific to this action, appended to genesis changes defined in chain config genesisChanges string - skipGentx bool + isConsumer bool } type StartChainValidator struct { @@ -133,7 +133,7 @@ func (tr TestRun) startChain( cmd := exec.Command("docker", "exec", tr.containerConfig.instanceName, "/bin/bash", "/testnet-scripts/start-chain.sh", chainConfig.binaryName, string(vals), string(chainConfig.chainId), chainConfig.ipPrefix, genesisChanges, - fmt.Sprint(action.skipGentx), + fmt.Sprint(action.isConsumer), // override config/config.toml for each node on chain // usually timeout_commit and peer_gossip_sleep_duration are changed to vary the test run duration // lower timeout_commit means the blocks are produced faster making the test run shorter @@ -170,6 +170,7 @@ func (tr TestRun) startChain( tr.addChainToRelayer(addChainToRelayerAction{ chain: action.chain, validator: action.validators[0].id, + consumer: action.isConsumer, }, verbose) } @@ -280,6 +281,8 @@ func (tr TestRun) submitConsumerAdditionProposal( if err != nil { log.Fatal(err, "\n", string(bz)) } + + tr.waitBlocks(action.chain, 1, 5*time.Second) } type submitConsumerRemovalProposalAction struct { @@ -521,7 +524,7 @@ func (tr TestRun) voteGovProposal( } wg.Wait() - time.Sleep(time.Duration(tr.chainConfigs[action.chain].votingWaitTime) * time.Second) + time.Sleep((time.Duration(tr.chainConfigs[action.chain].votingWaitTime)) * time.Second) } type startConsumerChainAction struct { @@ -564,7 +567,7 @@ func (tr TestRun) startConsumerChain( chain: action.consumerChain, validators: action.validators, genesisChanges: consumerGenesis, - skipGentx: true, + isConsumer: true, }, verbose) } @@ -698,6 +701,7 @@ func (tr TestRun) startChangeover( type addChainToRelayerAction struct { chain chainID validator validatorID + consumer bool } const hermesChainConfigTemplate = ` @@ -715,6 +719,7 @@ rpc_timeout = "10s" store_prefix = "ibc" trusting_period = "14days" websocket_addr = "%s" +ccv_consumer_chain = %v [chains.gas_price] denom = "stake" @@ -813,7 +818,7 @@ func (tr TestRun) addChainToHermes( keyName, rpcAddr, wsAddr, - // action.consumer, + action.consumer, ) bashCommand := fmt.Sprintf(`echo '%s' >> %s`, chainConfig, "/root/.hermes/config.toml") @@ -827,7 +832,16 @@ func (tr TestRun) addChainToHermes( } // Save mnemonic to file within container - saveMnemonicCommand := fmt.Sprintf(`echo '%s' > %s`, tr.validatorConfigs[action.validator].mnemonic, "/root/.hermes/mnemonic.txt") + var mnemonic string + if tr.validatorConfigs[action.validator].useConsumerKey && action.consumer { + mnemonic = tr.validatorConfigs[action.validator].consumerMnemonic + } else { + mnemonic = tr.validatorConfigs[action.validator].mnemonic + } + + saveMnemonicCommand := fmt.Sprintf(`echo '%s' > %s`, mnemonic, "/root/.hermes/mnemonic.txt") + fmt.Println("Add to hermes", action.validator) + fmt.Println(mnemonic) //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. bz, err = exec.Command("docker", "exec", tr.containerConfig.instanceName, "bash", "-c", saveMnemonicCommand, @@ -1767,6 +1781,8 @@ func (tr TestRun) assignConsumerPubKey(action assignConsumerPubKeyAction, verbos valCfg.useConsumerKey = true tr.validatorConfigs[action.validator] = valCfg } + + time.Sleep(1 * time.Second) } // slashThrottleDequeue polls slash queue sizes until nextQueueSize is achieved diff --git a/tests/e2e/actions_consumer_misbehaviour.go b/tests/e2e/actions_consumer_misbehaviour.go new file mode 100644 index 0000000000..2b01c2818e --- /dev/null +++ b/tests/e2e/actions_consumer_misbehaviour.go @@ -0,0 +1,92 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os/exec" + "time" +) + +type forkConsumerChainAction struct { + consumerChain chainID + providerChain chainID + validator validatorID + relayerConfig string +} + +func (tr TestRun) forkConsumerChain(action forkConsumerChainAction, verbose bool) { + valCfg := tr.validatorConfigs[action.validator] + + //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. + configureNodeCmd := exec.Command("docker", "exec", tr.containerConfig.instanceName, "/bin/bash", + "/testnet-scripts/fork-consumer.sh", tr.chainConfigs[action.consumerChain].binaryName, + string(action.validator), string(action.consumerChain), + tr.chainConfigs[action.consumerChain].ipPrefix, + tr.chainConfigs[action.providerChain].ipPrefix, + valCfg.mnemonic, + action.relayerConfig, + ) + + if verbose { + fmt.Println("forkConsumerChain - reconfigure node cmd:", configureNodeCmd.String()) + } + + cmdReader, err := configureNodeCmd.StdoutPipe() + if err != nil { + log.Fatal(err) + } + configureNodeCmd.Stderr = configureNodeCmd.Stdout + + if err := configureNodeCmd.Start(); err != nil { + log.Fatal(err) + } + + scanner := bufio.NewScanner(cmdReader) + + for scanner.Scan() { + out := scanner.Text() + if verbose { + fmt.Println("fork consumer validator : " + out) + } + if out == done { + break + } + } + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + + time.Sleep(5 * time.Second) +} + +type updateLightClientAction struct { + hostChain chainID + relayerConfig string + clientID string +} + +func (tr TestRun) updateLightClient( + action updateLightClientAction, + verbose bool, +) { + // hermes clear packets ibc0 transfer channel-13 + //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. + cmd := exec.Command("docker", "exec", tr.containerConfig.instanceName, "hermes", + "--config", action.relayerConfig, + "update", + "client", + "--client", action.clientID, + "--host-chain", string(action.hostChain), + ) + if verbose { + log.Println("updateLightClientAction cmd:", cmd.String()) + } + + bz, err := cmd.CombinedOutput() + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + tr.waitBlocks(action.hostChain, 5, 30*time.Second) +} diff --git a/tests/e2e/config.go b/tests/e2e/config.go index 3038f69f4e..efed270ce7 100644 --- a/tests/e2e/config.go +++ b/tests/e2e/config.go @@ -376,6 +376,90 @@ func ChangeoverTestRun() TestRun { } } +func ConsumerMisbehaviourTestRun() TestRun { + return TestRun{ + name: "misbehaviour", + containerConfig: ContainerConfig{ + containerName: "interchain-security-container", + instanceName: "interchain-security-instance", + ccvVersion: "1", + now: time.Now(), + }, + validatorConfigs: map[validatorID]ValidatorConfig{ + validatorID("alice"): { + mnemonic: "pave immune ethics wrap gain ceiling always holiday employ earth tumble real ice engage false unable carbon equal fresh sick tattoo nature pupil nuclear", + delAddress: "cosmos19pe9pg5dv9k5fzgzmsrgnw9rl9asf7ddwhu7lm", + valoperAddress: "cosmosvaloper19pe9pg5dv9k5fzgzmsrgnw9rl9asf7ddtrgtng", + valconsAddress: "cosmosvalcons1qmq08eruchr5sf5s3rwz7djpr5a25f7xw4mceq", + privValidatorKey: `{"address":"06C0F3E47CC5C748269088DC2F36411D3AAA27C6","pub_key":{"type":"tendermint/PubKeyEd25519","value":"RrclQz9bIhkIy/gfL485g3PYMeiIku4qeo495787X10="},"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"uX+ZpDMg89a6gtqs/+MQpCTSqlkZ0nJQJOhLlCJvwvdGtyVDP1siGQjL+B8vjzmDc9gx6IiS7ip6jj3nvztfXQ=="}}`, + nodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"fjw4/DAhyRPnwKgXns5SV7QfswRSXMWJpHS7TyULDmJ8ofUc5poQP8dgr8bZRbCV5RV8cPqDq3FPdqwpmUbmdA=="}}`, + ipSuffix: "4", + + // consumer chain assigned key + consumerMnemonic: "exile install vapor thing little toss immune notable lounge december final easy strike title end program interest quote cloth forget forward job october twenty", + consumerDelAddress: "cosmos1eeeggku6dzk3mv7wph3zq035rhtd890sjswszd", + consumerValoperAddress: "cosmosvaloper1eeeggku6dzk3mv7wph3zq035rhtd890shy69w7", + consumerValconsAddress: "cosmosvalcons1muys5jyqk4xd27e208nym85kn0t4zjcfeu63fe", + consumerValPubKey: `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"ujY14AgopV907IYgPAk/5x8c9267S4fQf89nyeCPTes="}`, + consumerPrivValidatorKey: `{"address":"DF090A4880B54CD57B2A79E64D9E969BD7514B09","pub_key":{"type":"tendermint/PubKeyEd25519","value":"ujY14AgopV907IYgPAk/5x8c9267S4fQf89nyeCPTes="},"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"TRJgf7lkTjs/sj43pyweEOanyV7H7fhnVivOi0A4yjW6NjXgCCilX3TshiA8CT/nHxz3brtLh9B/z2fJ4I9N6w=="}}`, + consumerNodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"F966RL9pi20aXRzEBe4D0xRQJtZt696Xxz44XUON52cFc83FMn1WXJbP6arvA2JPyn2LA3DLKCFHSgALrCGXGA=="}}`, + useConsumerKey: true, + }, + validatorID("bob"): { + mnemonic: "glass trip produce surprise diamond spin excess gaze wash drum human solve dress minor artefact canoe hard ivory orange dinner hybrid moral potato jewel", + delAddress: "cosmos1dkas8mu4kyhl5jrh4nzvm65qz588hy9qcz08la", + valoperAddress: "cosmosvaloper1dkas8mu4kyhl5jrh4nzvm65qz588hy9qakmjnw", + valconsAddress: "cosmosvalcons1nx7n5uh0ztxsynn4sje6eyq2ud6rc6klc96w39", + privValidatorKey: `{"address":"99BD3A72EF12CD024E7584B3AC900AE3743C6ADF","pub_key":{"type":"tendermint/PubKeyEd25519","value":"mAN6RXYxSM4MNGSIriYiS7pHuwAcOHDQAy9/wnlSzOI="},"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"QePcwfWtOavNK7pBJrtoLMzarHKn6iBWfWPFeyV+IdmYA3pFdjFIzgw0ZIiuJiJLuke7ABw4cNADL3/CeVLM4g=="}}`, + nodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"TQ4vHcO/vKdzGtWpelkX53WdMQd4kTsWGFrdcatdXFvWyO215Rewn5IRP0FszPLWr2DqPzmuH8WvxYGk5aeOXw=="}}`, + ipSuffix: "5", + + // consumer chain assigned key + consumerMnemonic: "grunt list hour endless observe better spoil penalty lab duck only layer vague fantasy satoshi record demise topple space shaft solar practice donor sphere", + consumerDelAddress: "cosmos1q90l6j6lzzgt460ehjj56azknlt5yrd4s38n97", + consumerValoperAddress: "cosmosvaloper1q90l6j6lzzgt460ehjj56azknlt5yrd449nxfd", + consumerValconsAddress: "cosmosvalcons1uuec3cjxajv5te08p220usrjhkfhg9wyvqn0tm", + consumerValPubKey: `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"QlG+iYe6AyYpvY1z9RNJKCVlH14Q/qSz4EjGdGCru3o="}`, + consumerPrivValidatorKey: `{"address":"E73388E246EC9945E5E70A94FE4072BD937415C4","pub_key":{"type":"tendermint/PubKeyEd25519","value":"QlG+iYe6AyYpvY1z9RNJKCVlH14Q/qSz4EjGdGCru3o="},"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"OFR4w+FC6EMw5fAGTrHVexyPrjzQ7QfqgZOMgVf0izlCUb6Jh7oDJim9jXP1E0koJWUfXhD+pLPgSMZ0YKu7eg=="}}`, + consumerNodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"uhPCqnL2KE8m/8OFNLQ5bN3CJr6mds+xfBi0E4umT/s2uWiJhet+vbYx88DHSdof3gGFNTIzAIxSppscBKX96w=="}}`, + useConsumerKey: false, + }, + }, + chainConfigs: map[chainID]ChainConfig{ + chainID("provi"): { + chainId: chainID("provi"), + binaryName: "interchain-security-pd", + ipPrefix: "7.7.7", + votingWaitTime: 20, + genesisChanges: ".app_state.gov.voting_params.voting_period = \"20s\" | " + + // Custom slashing parameters for testing validator downtime functionality + // See https://docs.cosmos.network/main/modules/slashing/04_begin_block.html#uptime-tracking + ".app_state.slashing.params.signed_blocks_window = \"10\" | " + + ".app_state.slashing.params.min_signed_per_window = \"0.500000000000000000\" | " + + ".app_state.slashing.params.downtime_jail_duration = \"2s\" | " + + ".app_state.slashing.params.slash_fraction_downtime = \"0.010000000000000000\" | " + + ".app_state.provider.params.slash_meter_replenish_fraction = \"1.0\" | " + // This disables slash packet throttling + ".app_state.provider.params.slash_meter_replenish_period = \"3s\"", + }, + chainID("consu"): { + chainId: chainID("consu"), + binaryName: "interchain-security-cd", + ipPrefix: "7.7.8", + votingWaitTime: 20, + genesisChanges: ".app_state.gov.voting_params.voting_period = \"20s\" | " + + ".app_state.slashing.params.signed_blocks_window = \"15\" | " + + ".app_state.slashing.params.min_signed_per_window = \"0.500000000000000000\" | " + + ".app_state.slashing.params.downtime_jail_duration = \"2s\" | " + + ".app_state.slashing.params.slash_fraction_downtime = \"0.010000000000000000\"", + }, + }, + tendermintConfigOverride: `s/timeout_commit = "5s"/timeout_commit = "1s"/;` + + `s/peer_gossip_sleep_duration = "100ms"/peer_gossip_sleep_duration = "50ms"/;` + + // Required to start consumer chain by running a single big validator + `s/fast_sync = true/fast_sync = false/;`, + } +} + func (s *TestRun) SetDockerConfig(localSdkPath string, useGaia bool, gaiaTag string) { if localSdkPath != "" { fmt.Println("USING LOCAL SDK", localSdkPath) diff --git a/tests/e2e/main.go b/tests/e2e/main.go index 737a318de2..4c7b6722cd 100644 --- a/tests/e2e/main.go +++ b/tests/e2e/main.go @@ -62,6 +62,7 @@ func main() { {DemocracyTestRun(true), democracySteps}, {DemocracyTestRun(false), rewardDenomConsumerSteps}, {SlashThrottleTestRun(), slashThrottleSteps}, + {ConsumerMisbehaviourTestRun(), consumerMisbehaviourSteps}, } if includeMultiConsumer != nil && *includeMultiConsumer { testRuns = append(testRuns, testRunWithSteps{MultiConsumerTestRun(), multipleConsumers}) @@ -173,6 +174,10 @@ func (tr *TestRun) runStep(step Step, verbose bool) { tr.startRelayer(action, verbose) case registerConsumerRewardDenomAction: tr.registerConsumerRewardDenom(action, verbose) + case forkConsumerChainAction: + tr.forkConsumerChain(action, verbose) + case updateLightClientAction: + tr.updateLightClient(action, verbose) default: log.Fatalf("unknown action in testRun %s: %#v", tr.name, action) } diff --git a/tests/e2e/state.go b/tests/e2e/state.go index 15500dd01f..8d9ba9a81e 100644 --- a/tests/e2e/state.go +++ b/tests/e2e/state.go @@ -28,6 +28,7 @@ type ChainState struct { ConsumerChainQueueSizes *map[chainID]uint GlobalSlashQueueSize *uint RegisteredConsumerRewardDenoms *[]string + ClientsFrozenHeights *map[string]clienttypes.Height } type Proposal interface { @@ -184,6 +185,14 @@ func (tr TestRun) getChainState(chain chainID, modelState ChainState) ChainState chainState.RegisteredConsumerRewardDenoms = ®isteredConsumerRewardDenoms } + if modelState.ClientsFrozenHeights != nil { + chainClientsFrozenHeights := map[string]clienttypes.Height{} + for id := range *modelState.ClientsFrozenHeights { + chainClientsFrozenHeights[id] = tr.getClientFrozenHeight(chain, id) + } + chainState.ClientsFrozenHeights = &chainClientsFrozenHeights + } + return chainState } @@ -737,3 +746,33 @@ func (tr TestRun) getQueryNodeIP(chain chainID) string { } return fmt.Sprintf("%s.253", tr.chainConfigs[chain].ipPrefix) } + +// getClientFrozenHeight returns the frozen height for a client with the given client ID +// by querying the hosting chain with the given chainID +func (tr TestRun) getClientFrozenHeight(chain chainID, clientID string) clienttypes.Height { + //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. + cmd := exec.Command("docker", "exec", tr.containerConfig.instanceName, tr.chainConfigs[chainID("provi")].binaryName, + "query", "ibc", "client", "state", clientID, + `--node`, tr.getQueryNode(chainID("provi")), + `-o`, `json`, + ) + + bz, err := cmd.CombinedOutput() + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + frozenHeight := gjson.Get(string(bz), "client_state.frozen_height") + + revHeight, err := strconv.Atoi(frozenHeight.Get("revision_height").String()) + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + revNumber, err := strconv.Atoi(frozenHeight.Get("revision_number").String()) + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + return clienttypes.Height{RevisionHeight: uint64(revHeight), RevisionNumber: uint64(revNumber)} +} diff --git a/tests/e2e/steps.go b/tests/e2e/steps.go index 7613b05558..78a56654e6 100644 --- a/tests/e2e/steps.go +++ b/tests/e2e/steps.go @@ -87,3 +87,10 @@ var changeoverSteps = concatSteps( stepsPostChangeoverDelegate("sover"), ) + +var consumerMisbehaviourSteps = concatSteps( + // start provider and consumer chain + stepsStartChainsWithSoftOptOut("consu"), + // make consumer validator to misbehave and get jail + stepsCauseConsumerMisbehaviour("consu"), +) diff --git a/tests/e2e/steps_consumer_misbehaviour.go b/tests/e2e/steps_consumer_misbehaviour.go new file mode 100644 index 0000000000..832f72081f --- /dev/null +++ b/tests/e2e/steps_consumer_misbehaviour.go @@ -0,0 +1,263 @@ +package main + +import ( + clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" +) + +// starts a provider chain and a consumer chain with two validators, +// where the voting power is distributed in order that the smallest validator +// can soft opt-out of validating the consumer chain. +func stepsStartChainsWithSoftOptOut(consumerName string) []Step { + s := []Step{ + { + // Create a provider chain with two validators, where one validator holds 96% of the voting power + // and the other validator holds 4% of the voting power. + action: StartChainAction{ + chain: chainID("provi"), + validators: []StartChainValidator{ + {id: validatorID("alice"), stake: 500000000, allocation: 10000000000}, + {id: validatorID("bob"), stake: 20000000, allocation: 10000000000}, + }, + }, + state: State{ + chainID("provi"): ChainState{ + ValBalances: &map[validatorID]uint{ + validatorID("alice"): 9500000000, + validatorID("bob"): 9980000000, + }, + }, + }, + }, + { + action: submitConsumerAdditionProposalAction{ + chain: chainID("provi"), + from: validatorID("alice"), + deposit: 10000001, + consumerChain: chainID(consumerName), + spawnTime: 0, + initialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1}, + }, + state: State{ + chainID("provi"): ChainState{ + ValBalances: &map[validatorID]uint{ + validatorID("alice"): 9489999999, + validatorID("bob"): 9980000000, + }, + Proposals: &map[uint]Proposal{ + 1: ConsumerAdditionProposal{ + Deposit: 10000001, + Chain: chainID(consumerName), + SpawnTime: 0, + InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1}, + Status: "PROPOSAL_STATUS_VOTING_PERIOD", + }, + }, + }, + }, + }, + // add a consumer key before the chain starts + // the key will be present in consumer genesis initial_val_set + { + action: assignConsumerPubKeyAction{ + chain: chainID(consumerName), + validator: validatorID("alice"), + consumerPubkey: `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"ujY14AgopV907IYgPAk/5x8c9267S4fQf89nyeCPTes="}`, + // consumer chain has not started + // we don't need to reconfigure the node + // since it will start with consumer key + reconfigureNode: false, + }, + state: State{ + chainID(consumerName): ChainState{ + AssignedKeys: &map[validatorID]string{ + validatorID("alice"): "cosmosvalcons1muys5jyqk4xd27e208nym85kn0t4zjcfeu63fe", + }, + ProviderKeys: &map[validatorID]string{ + validatorID("alice"): "cosmosvalcons1qmq08eruchr5sf5s3rwz7djpr5a25f7xw4mceq", + }, + }, + }, + }, + { + action: voteGovProposalAction{ + chain: chainID("provi"), + from: []validatorID{validatorID("alice"), validatorID("bob")}, + vote: []string{"yes", "yes"}, + propNumber: 1, + }, + state: State{ + chainID("provi"): ChainState{ + Proposals: &map[uint]Proposal{ + 1: ConsumerAdditionProposal{ + Deposit: 10000001, + Chain: chainID(consumerName), + SpawnTime: 0, + InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1}, + Status: "PROPOSAL_STATUS_PASSED", + }, + }, + ValBalances: &map[validatorID]uint{ + validatorID("alice"): 9500000000, + validatorID("bob"): 9980000000, + }, + }, + }, + }, + { + // start a consumer chain using a single big validator knowing that it holds more than 2/3 of the voting power + // and that the other validators hold less than 5% so they won't get jailed thanks to the sof opt-out mechanism. + action: startConsumerChainAction{ + consumerChain: chainID(consumerName), + providerChain: chainID("provi"), + validators: []StartChainValidator{ + {id: validatorID("alice"), stake: 500000000, allocation: 10000000000}, + }, + // For consumers that're launching with the provider being on an earlier version + // of ICS before the soft opt-out threshold was introduced, we need to set the + // soft opt-out threshold to 0.05 in the consumer genesis to ensure that the + // consumer binary doesn't panic. Sdk requires that all params are set to valid + // values from the genesis file. + genesisChanges: ".app_state.ccvconsumer.params.soft_opt_out_threshold = \"0.05\"", + }, + state: State{ + chainID("provi"): ChainState{ + ValBalances: &map[validatorID]uint{ + validatorID("alice"): 9500000000, + validatorID("bob"): 9980000000, + }, + }, + chainID(consumerName): ChainState{ + ValBalances: &map[validatorID]uint{ + validatorID("alice"): 10000000000, + }, + }, + }, + }, + { + action: addIbcConnectionAction{ + chainA: chainID(consumerName), + chainB: chainID("provi"), + clientA: 0, + clientB: 0, + }, + state: State{}, + }, + { + action: addIbcChannelAction{ + chainA: chainID(consumerName), + chainB: chainID("provi"), + connectionA: 0, + portA: "consumer", // TODO: check port mapping + portB: "provider", + order: "ordered", + }, + state: State{}, + }, + // delegate some token and relay the resulting VSC packets + // in oder to initiates the CCV channel + { + action: delegateTokensAction{ + chain: chainID("provi"), + from: validatorID("alice"), + to: validatorID("alice"), + amount: 11000000, + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 20, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 500, + validatorID("bob"): 20, + }, + }, + }, + }, + { + action: relayPacketsAction{ + chainA: chainID("provi"), + chainB: chainID(consumerName), + port: "provider", + channel: 0, + }, + state: State{ + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 20, + }, + }, + }, + }, + } + + return s +} + +// stepsCauseConsumerMisbehaviour forks a consumer chain in order to cause a ICS misbehaviour. +// The malicious validator behind the attack gets jailed and the consumer client freezed on the provider. +func stepsCauseConsumerMisbehaviour(consumerName string) []Step { + consumerClientID := "07-tendermint-0" + forkRelayerConfig := "/root/.hermes/config_fork.toml" + return []Step{ + { + // start a consumer chain's validator clone + action: forkConsumerChainAction{ + consumerChain: chainID(consumerName), + providerChain: chainID("provi"), + validator: validatorID("alice"), + relayerConfig: forkRelayerConfig, + }, + state: State{}, + }, + { + // start relayer to detect ICS misbehaviour + action: startRelayerAction{}, + state: State{ + // The consumer client shouldn't be frozen on the provider yet since + // no client update packet was sent by the fork yet + chainID("provi"): ChainState{ + ClientsFrozenHeights: &map[string]clienttypes.Height{ + "07-tendermint-0": { + RevisionNumber: 0, + RevisionHeight: 0, + }, + }, + }, + }, + }, + { + // update the consumer client hosted on the provider + // using the consumer fork as the primary node + action: updateLightClientAction{ + hostChain: chainID("provi"), + relayerConfig: forkRelayerConfig, // this relayer config uses the "forked" consumer + clientID: consumerClientID, + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 0, + validatorID("bob"): 20, + }, + ClientsFrozenHeights: &map[string]clienttypes.Height{ + "07-tendermint-0": { + RevisionNumber: 0, + RevisionHeight: 1, + }, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 20, + }, + }, + }, + }, + } +} diff --git a/tests/e2e/testnet-scripts/fork-consumer.sh b/tests/e2e/testnet-scripts/fork-consumer.sh new file mode 100644 index 0000000000..0bf96fcb79 --- /dev/null +++ b/tests/e2e/testnet-scripts/fork-consumer.sh @@ -0,0 +1,112 @@ +#!/bin/bash +set -eux + +# The gaiad binary +BIN=$1 + +# the validator ID used to perform the fork +VAL_ID=$2 + +# The consumer chain ID +CHAIN_ID=$3 + +# chain's IP address prefix; $PROV_CHAIN_PREFIX, $CONS_CHAIN_PREFIX... +# see chain config for details +CONS_CHAIN_PREFIX=$4 + +PROV_CHAIN_PREFIX=$5 + +VAL_MNEMONIC=$6 + +FORK_HERMES_CONFIG=$7 + +FORK_NODE_DIR=/$CHAIN_ID/validatorfork + +# create directory for forking/double-signing node +mkdir $FORK_NODE_DIR +cp -r /$CHAIN_ID/validator$VAL_ID/* $FORK_NODE_DIR + +# remove persistent peers +rm -f $FORK_NODE_DIR/addrbook.json + +# add fork to hermes relayer +tee $FORK_HERMES_CONFIG< mnemonic.txt + +# Start the validator forking the consumer chain +# using the sybil IP allocation +ip netns exec $CHAIN_ID-sybil $BIN \ + --home $FORK_NODE_DIR \ + --address tcp://$CONS_CHAIN_PREFIX.252:26655 \ + --rpc.laddr tcp://$CONS_CHAIN_PREFIX.252:26658 \ + --grpc.address $CONS_CHAIN_PREFIX.252:9091 \ + --log_level info \ + --p2p.laddr tcp://$CONS_CHAIN_PREFIX.252:26656 \ + --grpc-web.enable=false start &> /consu/validatorfork/logs & + diff --git a/tests/e2e/testnet-scripts/hermes-config.toml b/tests/e2e/testnet-scripts/hermes-config.toml index eb8154d95b..89c1f0a0bb 100644 --- a/tests/e2e/testnet-scripts/hermes-config.toml +++ b/tests/e2e/testnet-scripts/hermes-config.toml @@ -1,2 +1,18 @@ [global] - log_level = "info" \ No newline at end of file +log_level = "debug" + +[mode] + +[mode.clients] +enabled = true +refresh = true +misbehaviour = true + +[mode.connections] +enabled = false + +[mode.channels] +enabled = false + +[mode.packets] +enabled = true \ No newline at end of file diff --git a/tests/e2e/testnet-scripts/start-chain.sh b/tests/e2e/testnet-scripts/start-chain.sh index 9d6e73fdbb..8bc0dbadca 100644 --- a/tests/e2e/testnet-scripts/start-chain.sh +++ b/tests/e2e/testnet-scripts/start-chain.sh @@ -198,6 +198,7 @@ do #'s/foo/bar/;s/abc/def/' sed -i "$TENDERMINT_CONFIG_TRANSFORM" $CHAIN_ID/validator$VAL_ID/config/config.toml fi + done @@ -257,8 +258,12 @@ do fi done - # Remove leading comma and concat to flag - PERSISTENT_PEERS="--p2p.persistent_peers ${PERSISTENT_PEERS:1}" + + if [ "$PERSISTENT_PEERS" != "" ]; then + # Remove leading comma and concat to flag + PERSISTENT_PEERS="--p2p.persistent_peers ${PERSISTENT_PEERS:1}" + fi + ARGS="$GAIA_HOME $LISTEN_ADDRESS $RPC_ADDRESS $GRPC_ADDRESS $LOG_LEVEL $P2P_ADDRESS $ENABLE_WEBGRPC $PERSISTENT_PEERS" if [[ "$USE_COMETMOCK" == "true" ]]; then diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 937df77ed0..6cddda1092 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -20,9 +20,10 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty logger := k.Logger(ctx) if err := k.clientKeeper.CheckMisbehaviourAndUpdateState(ctx, &misbehaviour); err != nil { + logger.Info("Misbehaviour rejected", err.Error()) + return err } - // Since the misbehaviour packet was received within the trusting period // w.r.t to the last trusted consensus it entails that the infraction age // isn't too old. see ibc-go/modules/light-clients/07-tendermint/types/misbehaviour_handle.go @@ -33,6 +34,8 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty return err } + logger.Info("ConstructLightClientEvidence", fmt.Sprintf("%#+v\n", evidence)) + // jail and tombstone the byzantine validators for _, v := range evidence.ByzantineValidators { // convert consumer consensus address @@ -71,7 +74,6 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty // ConstructLightClientEvidence constructs and returns a CometBFT Light Client Attack(LCA) evidence struct // from the given misbehaviour func (k Keeper) ConstructLightClientEvidence(ctx sdk.Context, misbehaviour ibctmtypes.Misbehaviour) (*tmtypes.LightClientAttackEvidence, error) { - // construct the trusted and conflicted light blocks trusted, err := headerToLightBlock(*misbehaviour.Header1) if err != nil {