From a77eea15c913d1f401ee4cbac0e08d5fbf39565c Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Mon, 14 Aug 2023 23:24:41 +0200 Subject: [PATCH 01/12] feat!: add ICS misbehaviour handling (#826) * define msg to submit misbehaviour to provider implement msg handling logic e2e test msg handling logic * wip: get byzantine validators in misbehavioiur handling * add tx handler * format HandleConsumerMisbehaviour * add tx handler * add debugging stuff * Add misbehaviour handler * create message for consumer double voting evidence * add DRAFT double vote handler * Add cli cmd for submit consumer double voting * Add double-vote handler * add last update * fix jailing * pass first jailing integration test * format tests * doc * save * update e2e tests' * fix typo and improve docs * remove unwanted tm evidence protofile * fix typos * update submit-consumer-misbehaviour cli description * check that header1 and header2 have the same TrustedValidators * feat: add e2e tests for ICS misbehaviour (#1118) * remove unwanted changes * fix hermes config with assigned key * revert unwanted changes * revert local setup * remove log file * typo * update doc * update ICS misbehaviour test * update ICS misbehaviour test * revert mixed commits * add doc * lint * update to handle only equivocations * improve doc * update doc * update E2E tests comment * optimize signatures check * doc * update e2e tests * linter * remove todo * Feat: avoid race condition in ICS misbehaviour handling (#1148) * remove unwanted changes * fix hermes config with assigned key * revert unwanted changes * revert local setup * remove log file * typo * update doc * update ICS misbehaviour test * update ICS misbehaviour test * revert mixed commits * update ICS misbehaviour test * update ICS misbehaviour test * Add test for MsgSubmitConsumerMisbehaviour parsing * fix linter * save progress * add CheckMisbehaviourAndUpdateState * update integration tests * typo * remove e2e tests from another PRs * cleaning' * Update x/ccv/provider/keeper/misbehaviour.go Co-authored-by: Anca Zamfir * Update x/ccv/provider/keeper/misbehaviour.go Co-authored-by: Anca Zamfir * update integration tests * save * save * nits * remove todo * lint * Update x/ccv/provider/keeper/misbehaviour.go --------- Co-authored-by: Anca Zamfir Co-authored-by: Marius Poke * Update x/ccv/provider/client/cli/tx.go Co-authored-by: Anca Zamfir * Update x/ccv/provider/client/cli/tx.go Co-authored-by: Anca Zamfir * add attributes to EventTypeSubmitConsumerMisbehaviour * Update x/ccv/provider/keeper/misbehaviour.go Co-authored-by: Anca Zamfir * Update x/ccv/provider/keeper/misbehaviour.go Co-authored-by: Anca Zamfir * apply review suggestions * fix docstring * Update x/ccv/provider/keeper/misbehaviour.go Co-authored-by: Anca Zamfir * fix link * apply review suggestions * update docstring --------- Co-authored-by: Anca Zamfir Co-authored-by: Marius Poke --- Dockerfile | 2 +- .../ccv/provider/v1/tx.proto | 18 +- tests/e2e/actions.go | 28 +- tests/e2e/actions_consumer_misbehaviour.go | 92 ++++ tests/e2e/config.go | 84 ++++ tests/e2e/main.go | 5 + tests/e2e/state.go | 39 ++ tests/e2e/steps.go | 7 + tests/e2e/steps_consumer_misbehaviour.go | 253 ++++++++++ tests/e2e/testnet-scripts/fork-consumer.sh | 112 +++++ tests/e2e/testnet-scripts/hermes-config.toml | 18 +- tests/e2e/testnet-scripts/start-chain.sh | 9 +- tests/integration/misbehaviour.go | 394 ++++++++++++++++ testutil/integration/debug_test.go | 16 + testutil/keeper/mocks.go | 55 +++ x/ccv/provider/client/cli/tx.go | 49 ++ x/ccv/provider/handler.go | 3 + x/ccv/provider/keeper/misbehaviour.go | 164 +++++++ x/ccv/provider/keeper/msg_server.go | 20 + x/ccv/provider/types/codec.go | 10 + x/ccv/provider/types/msg.go | 48 +- x/ccv/provider/types/tx.pb.go | 444 ++++++++++++++++-- x/ccv/types/events.go | 15 +- x/ccv/types/expected_keepers.go | 4 + 24 files changed, 1842 insertions(+), 47 deletions(-) create mode 100644 tests/e2e/actions_consumer_misbehaviour.go create mode 100644 tests/e2e/steps_consumer_misbehaviour.go create mode 100644 tests/e2e/testnet-scripts/fork-consumer.sh create mode 100644 tests/integration/misbehaviour.go create mode 100644 x/ccv/provider/keeper/misbehaviour.go 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/proto/interchain_security/ccv/provider/v1/tx.proto b/proto/interchain_security/ccv/provider/v1/tx.proto index 705e155317..61be3064ea 100644 --- a/proto/interchain_security/ccv/provider/v1/tx.proto +++ b/proto/interchain_security/ccv/provider/v1/tx.proto @@ -7,11 +7,13 @@ import "google/api/annotations.proto"; import "gogoproto/gogo.proto"; import "cosmos_proto/cosmos.proto"; import "google/protobuf/any.proto"; +import "ibc/lightclients/tendermint/v1/tendermint.proto"; // Msg defines the Msg service. service Msg { rpc AssignConsumerKey(MsgAssignConsumerKey) returns (MsgAssignConsumerKeyResponse); rpc RegisterConsumerRewardDenom(MsgRegisterConsumerRewardDenom) returns (MsgRegisterConsumerRewardDenomResponse); + rpc SubmitConsumerMisbehaviour(MsgSubmitConsumerMisbehaviour) returns (MsgSubmitConsumerMisbehaviourResponse); } message MsgAssignConsumerKey { @@ -42,4 +44,18 @@ message MsgRegisterConsumerRewardDenom { } // MsgRegisterConsumerRewardDenomResponse defines the Msg/RegisterConsumerRewardDenom response type. -message MsgRegisterConsumerRewardDenomResponse {} \ No newline at end of file +message MsgRegisterConsumerRewardDenomResponse {} + +// MsgSubmitConsumerMisbehaviour defines a message that reports a misbehaviour +// observed on a consumer chain +// Note that the misbheaviour' headers must contain the same trusted states +message MsgSubmitConsumerMisbehaviour { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + string submitter = 1; + // The Misbehaviour of the consumer chain wrapping + // two conflicting IBC headers + ibc.lightclients.tendermint.v1.Misbehaviour misbehaviour = 2; +} + +message MsgSubmitConsumerMisbehaviourResponse {} 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..7d7fda94b1 --- /dev/null +++ b/tests/e2e/steps_consumer_misbehaviour.go @@ -0,0 +1,253 @@ +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 causes a ICS misbehaviour by forking a consumer chain. +func stepsCauseConsumerMisbehaviour(consumerName string) []Step { + consumerClientID := "07-tendermint-0" + forkRelayerConfig := "/root/.hermes/config_fork.toml" + return []Step{ + { + // fork the consumer chain by cloning of its validator node + 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{}, + }, + { + // update the fork consumer client to create a light client attack + // which should trigger a ICS misbehaviour message + action: updateLightClientAction{ + hostChain: chainID("provi"), + relayerConfig: forkRelayerConfig, // this relayer config uses the "forked" consumer + clientID: consumerClientID, + }, + state: State{ + chainID("provi"): ChainState{ + // validator should be jailed on the provider + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 0, + validatorID("bob"): 20, + }, + // The consumer light client should not be frozen + ClientsFrozenHeights: &map[string]clienttypes.Height{ + "07-tendermint-0": { + RevisionNumber: 0, + RevisionHeight: 0, + }, + }, + }, + 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/tests/integration/misbehaviour.go b/tests/integration/misbehaviour.go new file mode 100644 index 0000000000..c3b590d5c1 --- /dev/null +++ b/tests/integration/misbehaviour.go @@ -0,0 +1,394 @@ +package integration + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" + + ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types" + tmtypes "github.com/tendermint/tendermint/types" +) + +// TestHandleConsumerMisbehaviour tests that handling a valid misbehaviour, +// with conflicting headers forming an equivocation, results in the jailing and tombstoning of the validators +func (s *CCVTestSuite) TestHandleConsumerMisbehaviour() { + s.SetupCCVChannel(s.path) + // required to have the consumer client revision height greater than 0 + s.SendEmptyVSCPacket() + + for _, v := range s.providerChain.Vals.Validators { + s.setDefaultValSigningInfo(*v) + } + + altTime := s.providerCtx().BlockTime().Add(time.Minute) + + clientHeight := s.consumerChain.LastHeader.TrustedHeight + clientTMValset := tmtypes.NewValidatorSet(s.consumerChain.Vals.Validators) + clientSigners := s.consumerChain.Signers + + misb := &ibctmtypes.Misbehaviour{ + ClientId: s.path.EndpointA.ClientID, + Header1: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + altTime, + clientTMValset, + clientTMValset, + clientTMValset, + clientSigners, + ), + // create a different header by changing the header timestamp only + // in order to create an equivocation, i.e. both headers have the same deterministic states + Header2: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + altTime.Add(10*time.Second), + clientTMValset, + clientTMValset, + clientTMValset, + clientSigners, + ), + } + + err := s.providerApp.GetProviderKeeper().HandleConsumerMisbehaviour(s.providerCtx(), *misb) + s.NoError(err) + + // verify that validators are jailed and tombstoned + for _, v := range clientTMValset.Validators { + consuAddr := sdk.ConsAddress(v.Address.Bytes()) + provAddr := s.providerApp.GetProviderKeeper().GetProviderAddrFromConsumerAddr(s.providerCtx(), s.consumerChain.ChainID, types.NewConsumerConsAddress(consuAddr)) + val, ok := s.providerApp.GetTestStakingKeeper().GetValidatorByConsAddr(s.providerCtx(), provAddr.Address) + s.Require().True(ok) + s.Require().True(val.Jailed) + s.Require().True(s.providerApp.GetTestSlashingKeeper().IsTombstoned(s.providerCtx(), provAddr.Address)) + } +} + +func (s *CCVTestSuite) TestGetByzantineValidators() { + s.SetupCCVChannel(s.path) + // required to have the consumer client revision height greater than 0 + s.SendEmptyVSCPacket() + + altTime := s.providerCtx().BlockTime().Add(time.Minute) + + clientHeight := s.consumerChain.LastHeader.TrustedHeight + clientTMValset := tmtypes.NewValidatorSet(s.consumerChain.Vals.Validators) + clientSigners := s.consumerChain.Signers + + // Create a validator set subset + altValset := tmtypes.NewValidatorSet(s.consumerChain.Vals.Validators[0:3]) + altSigners := make(map[string]tmtypes.PrivValidator, 1) + altSigners[clientTMValset.Validators[0].Address.String()] = clientSigners[clientTMValset.Validators[0].Address.String()] + altSigners[clientTMValset.Validators[1].Address.String()] = clientSigners[clientTMValset.Validators[1].Address.String()] + altSigners[clientTMValset.Validators[2].Address.String()] = clientSigners[clientTMValset.Validators[2].Address.String()] + + // TODO: figure out how to test an amnesia cases for "amnesia" attack + testCases := []struct { + name string + misbehaviour *ibctmtypes.Misbehaviour + expByzantineValidators []*tmtypes.Validator + expPass bool + }{ + { + "invalid misbehaviour - Header1 is empty", + &ibctmtypes.Misbehaviour{ + Header1: &ibctmtypes.Header{}, + Header2: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + altTime, + altValset, + altValset, + clientTMValset, + altSigners, + ), + }, + nil, + false, + }, + { + "invalid headers - Header2 is empty", + &ibctmtypes.Misbehaviour{ + Header1: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + altTime, + clientTMValset, + clientTMValset, + clientTMValset, + clientSigners, + ), + Header2: &ibctmtypes.Header{}, + }, + nil, + false, + }, + { + "invalid light client attack - lunatic attack", + &ibctmtypes.Misbehaviour{ + ClientId: s.path.EndpointA.ClientID, + Header1: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + altTime, + clientTMValset, + clientTMValset, + clientTMValset, + clientSigners, + ), + Header2: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + altTime, + altValset, + altValset, + clientTMValset, + altSigners, + ), + }, + // Expect to get only the validators + // who signed both headers are returned + altValset.Validators, + true, + }, + { + "valid light client attack - equivocation", + &ibctmtypes.Misbehaviour{ + ClientId: s.path.EndpointA.ClientID, + Header1: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + altTime, + clientTMValset, + clientTMValset, + clientTMValset, + clientSigners, + ), + Header2: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + altTime.Add(time.Minute), + clientTMValset, + clientTMValset, + clientTMValset, + clientSigners, + ), + }, + // Expect to get the entire valset since + // all validators double-signed + clientTMValset.Validators, + true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + byzantineValidators, err := s.providerApp.GetProviderKeeper().GetByzantineValidators( + s.providerCtx(), + *tc.misbehaviour, + ) + if tc.expPass { + s.NoError(err) + // For both lunatic and equivocation attack all the validators + // who signed the bad header (Header2) should be in returned in the evidence + h2Valset := tc.misbehaviour.Header2.ValidatorSet + + s.Equal(len(h2Valset.Validators), len(byzantineValidators)) + + vs, err := tmtypes.ValidatorSetFromProto(tc.misbehaviour.Header2.ValidatorSet) + s.NoError(err) + + for _, v := range tc.expByzantineValidators { + idx, _ := vs.GetByAddress(v.Address) + s.True(idx >= 0) + } + + } else { + s.Error(err) + } + }) + } +} + +func (s *CCVTestSuite) TestCheckMisbehaviour() { + s.SetupCCVChannel(s.path) + // required to have the consumer client revision height greater than 0 + s.SendEmptyVSCPacket() + + // create signing info for all validators + for _, v := range s.providerChain.Vals.Validators { + s.setDefaultValSigningInfo(*v) + } + + // create a new header timestamp + headerTs := s.providerCtx().BlockTime().Add(time.Minute) + + // get trusted validators and height + clientHeight := s.consumerChain.LastHeader.TrustedHeight + clientTMValset := tmtypes.NewValidatorSet(s.consumerChain.Vals.Validators) + clientSigners := s.consumerChain.Signers + + // create an alternative validator set using more than 1/3 of the trusted validator set + altValset := tmtypes.NewValidatorSet(s.consumerChain.Vals.Validators[0:2]) + altSigners := make(map[string]tmtypes.PrivValidator, 1) + altSigners[clientTMValset.Validators[0].Address.String()] = clientSigners[clientTMValset.Validators[0].Address.String()] + altSigners[clientTMValset.Validators[1].Address.String()] = clientSigners[clientTMValset.Validators[1].Address.String()] + testCases := []struct { + name string + misbehaviour *ibctmtypes.Misbehaviour + expPass bool + }{ + { + "client state not found - shouldn't pass", + &ibctmtypes.Misbehaviour{ + ClientId: "clientID", + Header1: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + headerTs, + clientTMValset, + clientTMValset, + clientTMValset, + clientSigners, + ), + Header2: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + headerTs, + altValset, + altValset, + clientTMValset, + altSigners, + ), + }, + false, + }, + { + "invalid misbehaviour with empty header1 - shouldn't pass", + &ibctmtypes.Misbehaviour{ + Header1: &ibctmtypes.Header{}, + Header2: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + headerTs, + altValset, + altValset, + clientTMValset, + altSigners, + ), + }, + false, + }, + { + "invalid misbehaviour with different header height - shouldn't pass", + &ibctmtypes.Misbehaviour{ + ClientId: s.path.EndpointA.ClientID, + Header1: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + headerTs, + clientTMValset, + clientTMValset, + clientTMValset, + clientSigners, + ), + Header2: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+2), + clientHeight, + headerTs, + altValset, + altValset, + clientTMValset, + altSigners, + ), + }, + false, + }, + { + "valid misbehaviour - should pass", + &ibctmtypes.Misbehaviour{ + ClientId: s.path.EndpointA.ClientID, + Header1: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + headerTs, + clientTMValset, + clientTMValset, + clientTMValset, + clientSigners, + ), + // create header using a different validator set + Header2: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + headerTs, + altValset, + altValset, + clientTMValset, + altSigners, + ), + }, + true, + }, + { + "valid misbehaviour with already frozen client - should pass", + &ibctmtypes.Misbehaviour{ + ClientId: s.path.EndpointA.ClientID, + Header1: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + headerTs, + clientTMValset, + clientTMValset, + clientTMValset, + clientSigners, + ), + // the resulting Header2 will have a different BlockID + // than Header1 since doesn't share the same valset and signers + Header2: s.consumerChain.CreateTMClientHeader( + s.consumerChain.ChainID, + int64(clientHeight.RevisionHeight+1), + clientHeight, + headerTs, + altValset, + altValset, + clientTMValset, + altSigners, + ), + }, + true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.providerApp.GetProviderKeeper().CheckMisbehaviour(s.providerCtx(), *tc.misbehaviour) + cs, ok := s.providerApp.GetIBCKeeper().ClientKeeper.GetClientState(s.providerCtx(), s.path.EndpointA.ClientID) + s.Require().True(ok) + // verify that the client wasn't frozen + s.Require().Zero(cs.(*ibctmtypes.ClientState).FrozenHeight) + if tc.expPass { + s.NoError(err) + } else { + s.Error(err) + } + }) + } +} diff --git a/testutil/integration/debug_test.go b/testutil/integration/debug_test.go index b6456663a9..2d9421300b 100644 --- a/testutil/integration/debug_test.go +++ b/testutil/integration/debug_test.go @@ -256,3 +256,19 @@ func TestQueueAndSendVSCMaturedPackets(t *testing.T) { func TestRecycleTransferChannel(t *testing.T) { runCCVTestByName(t, "TestRecycleTransferChannel") } + +// +// Misbehaviour test +// + +func TestHandleConsumerMisbehaviour(t *testing.T) { + runCCVTestByName(t, "TestHandleConsumerMisbehaviour") +} + +func TestGetByzantineValidators(t *testing.T) { + runCCVTestByName(t, "TestGetByzantineValidators") +} + +func TestCheckMisbehaviour(t *testing.T) { + runCCVTestByName(t, "TestCheckMisbehaviour") +} diff --git a/testutil/keeper/mocks.go b/testutil/keeper/mocks.go index 4f931d8152..1e792e3a4a 100644 --- a/testutil/keeper/mocks.go +++ b/testutil/keeper/mocks.go @@ -678,6 +678,34 @@ func (m *MockClientKeeper) EXPECT() *MockClientKeeperMockRecorder { return m.recorder } +// CheckMisbehaviourAndUpdateState mocks base method. +func (m *MockClientKeeper) CheckMisbehaviourAndUpdateState(ctx types.Context, misbehaviour exported.Misbehaviour) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckMisbehaviourAndUpdateState", ctx, misbehaviour) + ret0, _ := ret[0].(error) + return ret0 +} + +// CheckMisbehaviourAndUpdateState indicates an expected call of CheckMisbehaviourAndUpdateState. +func (mr *MockClientKeeperMockRecorder) CheckMisbehaviourAndUpdateState(ctx, misbehaviour interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckMisbehaviourAndUpdateState", reflect.TypeOf((*MockClientKeeper)(nil).CheckMisbehaviourAndUpdateState), ctx, misbehaviour) +} + +// ClientStore mocks base method. +func (m *MockClientKeeper) ClientStore(ctx types.Context, clientID string) types.KVStore { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClientStore", ctx, clientID) + ret0, _ := ret[0].(types.KVStore) + return ret0 +} + +// ClientStore indicates an expected call of ClientStore. +func (mr *MockClientKeeperMockRecorder) ClientStore(ctx, clientID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientStore", reflect.TypeOf((*MockClientKeeper)(nil).ClientStore), ctx, clientID) +} + // CreateClient mocks base method. func (m *MockClientKeeper) CreateClient(ctx types.Context, clientState exported.ClientState, consensusState exported.ConsensusState) (string, error) { m.ctrl.T.Helper() @@ -693,6 +721,21 @@ func (mr *MockClientKeeperMockRecorder) CreateClient(ctx, clientState, consensus return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateClient", reflect.TypeOf((*MockClientKeeper)(nil).CreateClient), ctx, clientState, consensusState) } +// GetClientConsensusState mocks base method. +func (m *MockClientKeeper) GetClientConsensusState(ctx types.Context, clientID string, height exported.Height) (exported.ConsensusState, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClientConsensusState", ctx, clientID, height) + ret0, _ := ret[0].(exported.ConsensusState) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// GetClientConsensusState indicates an expected call of GetClientConsensusState. +func (mr *MockClientKeeperMockRecorder) GetClientConsensusState(ctx, clientID, height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClientConsensusState", reflect.TypeOf((*MockClientKeeper)(nil).GetClientConsensusState), ctx, clientID, height) +} + // GetClientState mocks base method. func (m *MockClientKeeper) GetClientState(ctx types.Context, clientID string) (exported.ClientState, bool) { m.ctrl.T.Helper() @@ -775,6 +818,18 @@ func (mr *MockDistributionKeeperMockRecorder) FundCommunityPool(ctx, amount, sen return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FundCommunityPool", reflect.TypeOf((*MockDistributionKeeper)(nil).FundCommunityPool), ctx, amount, sender) } +// SetClientState mocks base method. +func (m *MockClientKeeper) SetClientState(ctx types.Context, clientID string, clientState exported.ClientState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetClientState", ctx, clientID, clientState) +} + +// SetClientState indicates an expected call of SetClientState. +func (mr *MockClientKeeperMockRecorder) SetClientState(ctx, clientID, clientState interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetClientState", reflect.TypeOf((*MockClientKeeper)(nil).SetClientState), ctx, clientID, clientState) +} + // MockConsumerHooks is a mock of ConsumerHooks interface. type MockConsumerHooks struct { ctrl *gomock.Controller diff --git a/x/ccv/provider/client/cli/tx.go b/x/ccv/provider/client/cli/tx.go index 73b1df34c3..1cad8b38ce 100644 --- a/x/ccv/provider/client/cli/tx.go +++ b/x/ccv/provider/client/cli/tx.go @@ -10,6 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/version" + ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" @@ -27,6 +28,7 @@ func GetTxCmd() *cobra.Command { cmd.AddCommand(NewAssignConsumerKeyCmd()) cmd.AddCommand(NewRegisterConsumerRewardDenomCmd()) + cmd.AddCommand(NewSubmitConsumerMisbehaviourCmd()) return cmd } @@ -99,3 +101,50 @@ $ %s tx provider register-consumer-reward-denom untrn --from mykey return cmd } + +func NewSubmitConsumerMisbehaviourCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "submit-consumer-misbehaviour [misbehaviour]", + Short: "submit an IBC misbehaviour for a consumer chain", + Long: strings.TrimSpace( + fmt.Sprintf(`Submit an IBC misbehaviour detected on a consumer chain. +An IBC misbehaviour contains two conflicting IBC client headers, which are used to form a light client attack evidence. +The misbehaviour type definition can be found in the IBC client messages, see ibc-go/proto/ibc/core/client/v1/tx.proto. + +Examples: +%s tx provider submit-consumer-misbehaviour [path/to/misbehaviour.json] --from node0 --home ../node0 --chain-id $CID + `, version.AppName)), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + txf := tx.NewFactoryCLI(clientCtx, cmd.Flags()). + WithTxConfig(clientCtx.TxConfig).WithAccountRetriever(clientCtx.AccountRetriever) + + submitter := clientCtx.GetFromAddress() + var misbehaviour ibctmtypes.Misbehaviour + if err := clientCtx.Codec.UnmarshalInterfaceJSON([]byte(args[1]), &misbehaviour); err != nil { + return err + } + + msg, err := types.NewMsgSubmitConsumerMisbehaviour(submitter, &misbehaviour) + if err != nil { + return err + } + if err := msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txf, msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + _ = cmd.MarkFlagRequired(flags.FlagFrom) + + return cmd +} diff --git a/x/ccv/provider/handler.go b/x/ccv/provider/handler.go index dc0c8cbc4f..6fa38b5ddf 100644 --- a/x/ccv/provider/handler.go +++ b/x/ccv/provider/handler.go @@ -21,6 +21,9 @@ func NewHandler(k *keeper.Keeper) sdk.Handler { case *types.MsgRegisterConsumerRewardDenom: res, err := msgServer.RegisterConsumerRewardDenom(sdk.WrapSDKContext(ctx), msg) return sdk.WrapServiceResult(ctx, res, err) + case *types.MsgSubmitConsumerMisbehaviour: + res, err := msgServer.SubmitConsumerMisbehaviour(sdk.WrapSDKContext(ctx), msg) + return sdk.WrapServiceResult(ctx, res, err) default: return nil, errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", types.ModuleName, msg) } diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go new file mode 100644 index 0000000000..4d8f517e95 --- /dev/null +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -0,0 +1,164 @@ +package keeper + +import ( + "sort" + + "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + ibcclienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" + ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types" + tmtypes "github.com/tendermint/tendermint/types" +) + +// HandleConsumerMisbehaviour checks if the given IBC misbehaviour corresponds to an equivocation light client attack, +// and in this case, jails and tombstones the Byzantine validators +func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmtypes.Misbehaviour) error { + logger := k.Logger(ctx) + + // Check that the misbehaviour is valid and that the client consensus states at trusted heights are within trusting period + if err := k.CheckMisbehaviour(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 trusted consensus states the infraction age + // isn't too old. see ibc-go/modules/light-clients/07-tendermint/types/misbehaviour_handle.go + + // Get Byzantine validators from the conflicting headers + byzantineValidators, err := k.GetByzantineValidators(ctx, misbehaviour) + if err != nil { + return err + } + + // jail and tombstone the Byzantine validators + for _, v := range byzantineValidators { + // convert consumer consensus address + consumerAddr := types.NewConsumerConsAddress(sdk.ConsAddress(v.Address.Bytes())) + providerAddr := k.GetProviderAddrFromConsumerAddr(ctx, misbehaviour.Header1.Header.ChainID, consumerAddr) + val, ok := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) + + if !ok || val.IsUnbonded() { + logger.Error("validator not found or is unbonded", providerAddr.String()) + continue + } + + // jail validator if not already + if !val.IsJailed() { + k.stakingKeeper.Jail(ctx, providerAddr.ToSdkConsAddr()) + } + + // tombstone validator if not already + if !k.slashingKeeper.IsTombstoned(ctx, providerAddr.ToSdkConsAddr()) { + k.slashingKeeper.Tombstone(ctx, providerAddr.ToSdkConsAddr()) + k.Logger(ctx).Info("validator tombstoned", "provider cons addr", providerAddr.String()) + } + + // update jail time to end after double sign jail duration + k.slashingKeeper.JailUntil(ctx, providerAddr.ToSdkConsAddr(), evidencetypes.DoubleSignJailEndTime) + } + + logger.Info( + "confirmed equivocation light client attack", + "byzantine validators", byzantineValidators, + ) + + return nil +} + +// GetByzantineValidators returns the validators that signed both headers. +// If the misbehavior is an equivocation light client attack, then these +// validators are the Byzantine validators. +func (k Keeper) GetByzantineValidators(ctx sdk.Context, misbehaviour ibctmtypes.Misbehaviour) ([]*tmtypes.Validator, error) { + // construct the trusted and conflicted light blocks + lightBlock1, err := headerToLightBlock(*misbehaviour.Header1) + if err != nil { + return nil, err + } + lightBlock2, err := headerToLightBlock(*misbehaviour.Header2) + if err != nil { + return nil, err + } + + var validators []*tmtypes.Validator + + // compare the signatures of the headers + // and return the intersection of validators who signed both + + // create a map with the validators' address that signed header1 + header1Signers := map[string]struct{}{} + for _, sign := range lightBlock1.Commit.Signatures { + if sign.Absent() { + continue + } + header1Signers[sign.ValidatorAddress.String()] = struct{}{} + } + + // iterate over the header2 signers and check if they signed header1 + for _, sign := range lightBlock2.Commit.Signatures { + if sign.Absent() { + continue + } + if _, ok := header1Signers[sign.ValidatorAddress.String()]; ok { + _, val := lightBlock1.ValidatorSet.GetByAddress(sign.ValidatorAddress) + validators = append(validators, val) + } + } + + sort.Sort(tmtypes.ValidatorsByVotingPower(validators)) + return validators, nil +} + +// headerToLightBlock returns a CometBFT light block from the given IBC header +func headerToLightBlock(h ibctmtypes.Header) (*tmtypes.LightBlock, error) { + sh, err := tmtypes.SignedHeaderFromProto(h.SignedHeader) + if err != nil { + return nil, err + } + + vs, err := tmtypes.ValidatorSetFromProto(h.ValidatorSet) + if err != nil { + return nil, err + } + + return &tmtypes.LightBlock{ + SignedHeader: sh, + ValidatorSet: vs, + }, nil +} + +// CheckMisbehaviour checks that headers in the given misbehaviour forms +// a valid light client attack and that the corresponding light client isn't expired +func (k Keeper) CheckMisbehaviour(ctx sdk.Context, misbehaviour ibctmtypes.Misbehaviour) error { + if err := misbehaviour.ValidateBasic(); err != nil { + return err + } + + clientState, found := k.clientKeeper.GetClientState(ctx, misbehaviour.GetClientID()) + if !found { + return sdkerrors.Wrapf(ibcclienttypes.ErrClientNotFound, "cannot check misbehaviour for client with ID %s", misbehaviour.GetClientID()) + } + + clientStore := k.clientKeeper.ClientStore(ctx, misbehaviour.GetClientID()) + + // Check that the headers are at the same height to ensure that + // the misbehaviour is for a light client attack and not a time violation, + // see https://github.com/cosmos/ibc-go/blob/8f53c21361f9d65448a850c2eafcf3ab3c384a61/modules/light-clients/07-tendermint/types/misbehaviour_handle.go#L56 + if !misbehaviour.Header1.GetHeight().EQ(misbehaviour.Header2.GetHeight()) { + return sdkerrors.Wrap(ibcclienttypes.ErrInvalidMisbehaviour, "headers are not at same height") + } + + // CheckMisbehaviourAndUpdateState verifies the misbehaviour against the trusted consensus states + // but does NOT update the light client state. + // Note CheckMisbehaviourAndUpdateState returns an error if the trusted consensus states are expired + _, err := clientState.CheckMisbehaviourAndUpdateState(ctx, k.cdc, clientStore, &misbehaviour) + if err != nil { + return err + } + + return nil +} diff --git a/x/ccv/provider/keeper/msg_server.go b/x/ccv/provider/keeper/msg_server.go index ea5ff66220..80f325cc1b 100644 --- a/x/ccv/provider/keeper/msg_server.go +++ b/x/ccv/provider/keeper/msg_server.go @@ -126,3 +126,23 @@ func (k msgServer) RegisterConsumerRewardDenom(goCtx context.Context, msg *types return &types.MsgRegisterConsumerRewardDenomResponse{}, nil } + +func (k msgServer) SubmitConsumerMisbehaviour(goCtx context.Context, msg *types.MsgSubmitConsumerMisbehaviour) (*types.MsgSubmitConsumerMisbehaviourResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + if err := k.Keeper.HandleConsumerMisbehaviour(ctx, *msg.Misbehaviour); err != nil { + return &types.MsgSubmitConsumerMisbehaviourResponse{}, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + ccvtypes.EventTypeSubmitConsumerMisbehaviour, + sdk.NewAttribute(ccvtypes.AttributeConsumerMisbehaviour, msg.Misbehaviour.String()), + sdk.NewAttribute(ccvtypes.AttributeSubmitterAddress, msg.Submitter), + sdk.NewAttribute(ccvtypes.AttributeMisbehaviourClientId, msg.Misbehaviour.ClientId), + sdk.NewAttribute(ccvtypes.AttributeMisbehaviourHeight1, msg.Misbehaviour.Header1.GetHeight().String()), + sdk.NewAttribute(ccvtypes.AttributeMisbehaviourHeight2, msg.Misbehaviour.Header2.GetHeight().String()), + ), + }) + + return &types.MsgSubmitConsumerMisbehaviourResponse{}, nil +} diff --git a/x/ccv/provider/types/codec.go b/x/ccv/provider/types/codec.go index 3e4b34dd42..2f28b398f5 100644 --- a/x/ccv/provider/types/codec.go +++ b/x/ccv/provider/types/codec.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/msgservice" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/ibc-go/v4/modules/core/exported" ) // RegisterLegacyAminoCodec registers the necessary x/ibc transfer interfaces and concrete types @@ -36,6 +37,15 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) { &EquivocationProposal{}, ) + registry.RegisterImplementations( + (*sdk.Msg)(nil), + &MsgSubmitConsumerMisbehaviour{}, + ) + registry.RegisterInterface( + "ibc.core.client.v1.Misbehaviour", + (*exported.Misbehaviour)(nil), + ) + msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) } diff --git a/x/ccv/provider/types/msg.go b/x/ccv/provider/types/msg.go index 67ad99d10c..5c217f53b8 100644 --- a/x/ccv/provider/types/msg.go +++ b/x/ccv/provider/types/msg.go @@ -5,15 +5,21 @@ import ( "strings" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types" ) // provider message types const ( TypeMsgAssignConsumerKey = "assign_consumer_key" TypeMsgRegisterConsumerRewardDenom = "register_consumer_reward_denom" + TypeMsgSubmitConsumerMisbehaviour = "submit_consumer_misbehaviour" ) -var _ sdk.Msg = &MsgAssignConsumerKey{} +var ( + _ sdk.Msg = &MsgAssignConsumerKey{} + _ sdk.Msg = &MsgSubmitConsumerMisbehaviour{} +) // NewMsgAssignConsumerKey creates a new MsgAssignConsumerKey instance. // Delegator address and validator address are the same. @@ -139,3 +145,43 @@ func (msg MsgRegisterConsumerRewardDenom) ValidateBasic() error { return nil } + +func NewMsgSubmitConsumerMisbehaviour(submitter sdk.AccAddress, misbehaviour *ibctmtypes.Misbehaviour) (*MsgSubmitConsumerMisbehaviour, error) { + return &MsgSubmitConsumerMisbehaviour{Submitter: submitter.String(), Misbehaviour: misbehaviour}, nil +} + +// Route implements the sdk.Msg interface. +func (msg MsgSubmitConsumerMisbehaviour) Route() string { return RouterKey } + +// Type implements the sdk.Msg interface. +func (msg MsgSubmitConsumerMisbehaviour) Type() string { + return TypeMsgSubmitConsumerMisbehaviour +} + +// Type implements the sdk.Msg interface. +func (msg MsgSubmitConsumerMisbehaviour) ValidateBasic() error { + if msg.Submitter == "" { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Submitter) + } + + if err := msg.Misbehaviour.ValidateBasic(); err != nil { + return err + } + return nil +} + +// Type implements the sdk.Msg interface. +func (msg MsgSubmitConsumerMisbehaviour) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(&msg) + return sdk.MustSortJSON(bz) +} + +// Type implements the sdk.Msg interface. +func (msg MsgSubmitConsumerMisbehaviour) GetSigners() []sdk.AccAddress { + addr, err := sdk.AccAddressFromBech32(msg.Submitter) + if err != nil { + // same behavior as in cosmos-sdk + panic(err) + } + return []sdk.AccAddress{addr} +} diff --git a/x/ccv/provider/types/tx.pb.go b/x/ccv/provider/types/tx.pb.go index 3603695359..f27ab52533 100644 --- a/x/ccv/provider/types/tx.pb.go +++ b/x/ccv/provider/types/tx.pb.go @@ -7,6 +7,7 @@ import ( context "context" fmt "fmt" _ "github.com/cosmos/cosmos-sdk/codec/types" + types "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types" _ "github.com/gogo/protobuf/gogoproto" grpc1 "github.com/gogo/protobuf/grpc" proto "github.com/gogo/protobuf/proto" @@ -191,11 +192,92 @@ func (m *MsgRegisterConsumerRewardDenomResponse) XXX_DiscardUnknown() { var xxx_messageInfo_MsgRegisterConsumerRewardDenomResponse proto.InternalMessageInfo +// MsgSubmitConsumerMisbehaviour defines a message that reports a misbehaviour +// observed on a consumer chain +// Note that the misbheaviour' headers must contain the same trusted states +type MsgSubmitConsumerMisbehaviour struct { + Submitter string `protobuf:"bytes,1,opt,name=submitter,proto3" json:"submitter,omitempty"` + // The Misbehaviour of the consumer chain wrapping + // two conflicting IBC headers + Misbehaviour *types.Misbehaviour `protobuf:"bytes,2,opt,name=misbehaviour,proto3" json:"misbehaviour,omitempty"` +} + +func (m *MsgSubmitConsumerMisbehaviour) Reset() { *m = MsgSubmitConsumerMisbehaviour{} } +func (m *MsgSubmitConsumerMisbehaviour) String() string { return proto.CompactTextString(m) } +func (*MsgSubmitConsumerMisbehaviour) ProtoMessage() {} +func (*MsgSubmitConsumerMisbehaviour) Descriptor() ([]byte, []int) { + return fileDescriptor_43221a4391e9fbf4, []int{4} +} +func (m *MsgSubmitConsumerMisbehaviour) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgSubmitConsumerMisbehaviour) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgSubmitConsumerMisbehaviour.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgSubmitConsumerMisbehaviour) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgSubmitConsumerMisbehaviour.Merge(m, src) +} +func (m *MsgSubmitConsumerMisbehaviour) XXX_Size() int { + return m.Size() +} +func (m *MsgSubmitConsumerMisbehaviour) XXX_DiscardUnknown() { + xxx_messageInfo_MsgSubmitConsumerMisbehaviour.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgSubmitConsumerMisbehaviour proto.InternalMessageInfo + +type MsgSubmitConsumerMisbehaviourResponse struct { +} + +func (m *MsgSubmitConsumerMisbehaviourResponse) Reset() { *m = MsgSubmitConsumerMisbehaviourResponse{} } +func (m *MsgSubmitConsumerMisbehaviourResponse) String() string { return proto.CompactTextString(m) } +func (*MsgSubmitConsumerMisbehaviourResponse) ProtoMessage() {} +func (*MsgSubmitConsumerMisbehaviourResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_43221a4391e9fbf4, []int{5} +} +func (m *MsgSubmitConsumerMisbehaviourResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgSubmitConsumerMisbehaviourResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgSubmitConsumerMisbehaviourResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgSubmitConsumerMisbehaviourResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgSubmitConsumerMisbehaviourResponse.Merge(m, src) +} +func (m *MsgSubmitConsumerMisbehaviourResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgSubmitConsumerMisbehaviourResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgSubmitConsumerMisbehaviourResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgSubmitConsumerMisbehaviourResponse proto.InternalMessageInfo + func init() { proto.RegisterType((*MsgAssignConsumerKey)(nil), "interchain_security.ccv.provider.v1.MsgAssignConsumerKey") proto.RegisterType((*MsgAssignConsumerKeyResponse)(nil), "interchain_security.ccv.provider.v1.MsgAssignConsumerKeyResponse") proto.RegisterType((*MsgRegisterConsumerRewardDenom)(nil), "interchain_security.ccv.provider.v1.MsgRegisterConsumerRewardDenom") proto.RegisterType((*MsgRegisterConsumerRewardDenomResponse)(nil), "interchain_security.ccv.provider.v1.MsgRegisterConsumerRewardDenomResponse") + proto.RegisterType((*MsgSubmitConsumerMisbehaviour)(nil), "interchain_security.ccv.provider.v1.MsgSubmitConsumerMisbehaviour") + proto.RegisterType((*MsgSubmitConsumerMisbehaviourResponse)(nil), "interchain_security.ccv.provider.v1.MsgSubmitConsumerMisbehaviourResponse") } func init() { @@ -203,36 +285,43 @@ func init() { } var fileDescriptor_43221a4391e9fbf4 = []byte{ - // 453 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x93, 0x3d, 0x6b, 0x14, 0x41, - 0x18, 0xc7, 0x77, 0x13, 0xd4, 0x64, 0x8c, 0x82, 0xc3, 0x15, 0x97, 0xf3, 0xd8, 0xd3, 0x15, 0x24, - 0x85, 0xee, 0x90, 0x58, 0x88, 0x01, 0x8b, 0x4b, 0x6c, 0x24, 0x5c, 0xb3, 0x8d, 0x60, 0xe1, 0xb1, - 0x37, 0x33, 0x4e, 0x06, 0xb3, 0xf3, 0x2c, 0xf3, 0xcc, 0xad, 0xd9, 0x6f, 0x60, 0xa9, 0x95, 0x6d, - 0xbe, 0x81, 0x5f, 0x43, 0xb0, 0x49, 0x69, 0x25, 0x72, 0xd7, 0x58, 0xfb, 0x09, 0x64, 0xdf, 0x3c, - 0xc5, 0xe3, 0x08, 0x92, 0xee, 0x79, 0xdb, 0xff, 0xff, 0xb7, 0x33, 0xf3, 0x90, 0x07, 0xda, 0x38, - 0x69, 0xf9, 0x71, 0xa2, 0xcd, 0x18, 0x25, 0x9f, 0x5a, 0xed, 0x0a, 0xc6, 0x79, 0xce, 0x32, 0x0b, - 0xb9, 0x16, 0xd2, 0xb2, 0x7c, 0x97, 0xb9, 0xd3, 0x28, 0xb3, 0xe0, 0x80, 0xde, 0x5b, 0x32, 0x1d, - 0x71, 0x9e, 0x47, 0xed, 0x74, 0x94, 0xef, 0xf6, 0xfa, 0x0a, 0x40, 0x9d, 0x48, 0x96, 0x64, 0x9a, - 0x25, 0xc6, 0x80, 0x4b, 0x9c, 0x06, 0x83, 0xb5, 0x44, 0xaf, 0xa3, 0x40, 0x41, 0x15, 0xb2, 0x32, - 0x6a, 0xaa, 0xdb, 0x1c, 0x30, 0x05, 0x1c, 0xd7, 0x8d, 0x3a, 0x69, 0x5b, 0x8d, 0x5c, 0x95, 0x4d, - 0xa6, 0xaf, 0x59, 0x62, 0x8a, 0xba, 0x15, 0x7e, 0xf4, 0x49, 0x67, 0x84, 0x6a, 0x88, 0xa8, 0x95, - 0x39, 0x04, 0x83, 0xd3, 0x54, 0xda, 0x23, 0x59, 0xd0, 0x6d, 0xb2, 0x51, 0x43, 0x6a, 0xd1, 0xf5, - 0xef, 0xf8, 0x3b, 0x9b, 0xf1, 0xb5, 0x2a, 0x7f, 0x2e, 0xe8, 0x63, 0x72, 0xa3, 0x85, 0x1d, 0x27, - 0x42, 0xd8, 0xee, 0x5a, 0xd9, 0x3f, 0xa0, 0x3f, 0xbf, 0x0d, 0x6e, 0x16, 0x49, 0x7a, 0xb2, 0x1f, - 0x96, 0x55, 0x89, 0x18, 0xc6, 0x5b, 0xed, 0xe0, 0x50, 0x08, 0x4b, 0xef, 0x92, 0x2d, 0xde, 0x58, - 0x8c, 0xdf, 0xc8, 0xa2, 0xbb, 0x5e, 0xe9, 0x5e, 0xe7, 0x0b, 0xdb, 0xfd, 0x8d, 0x77, 0x67, 0x03, - 0xef, 0xc7, 0xd9, 0xc0, 0x0b, 0x03, 0xd2, 0x5f, 0x06, 0x16, 0x4b, 0xcc, 0xc0, 0xa0, 0x0c, 0x5f, - 0x91, 0x60, 0x84, 0x2a, 0x96, 0x4a, 0xa3, 0x93, 0xb6, 0x9d, 0x88, 0xe5, 0xdb, 0xc4, 0x8a, 0x67, - 0xd2, 0x40, 0x4a, 0x3b, 0xe4, 0x8a, 0x28, 0x83, 0x86, 0xbf, 0x4e, 0x68, 0x9f, 0x6c, 0x0a, 0x99, - 0x01, 0x6a, 0x07, 0x0d, 0x79, 0xbc, 0x28, 0xfc, 0xe1, 0xbf, 0x43, 0xee, 0xaf, 0xd6, 0x6f, 0x49, - 0xf6, 0xbe, 0xac, 0x91, 0xf5, 0x11, 0x2a, 0xfa, 0xc1, 0x27, 0xb7, 0xfe, 0x3d, 0xc8, 0x27, 0xd1, - 0x05, 0x6e, 0x3c, 0x5a, 0xf6, 0xab, 0xbd, 0xe1, 0x7f, 0x7f, 0xda, 0xb2, 0xd1, 0x4f, 0x3e, 0xb9, - 0xbd, 0xea, 0x8c, 0x0e, 0x2f, 0x6a, 0xb1, 0x42, 0xa4, 0x77, 0x74, 0x09, 0x22, 0x2d, 0xf1, 0xc1, - 0x8b, 0xcf, 0xb3, 0xc0, 0x3f, 0x9f, 0x05, 0xfe, 0xf7, 0x59, 0xe0, 0xbf, 0x9f, 0x07, 0xde, 0xf9, - 0x3c, 0xf0, 0xbe, 0xce, 0x03, 0xef, 0xe5, 0x53, 0xa5, 0xdd, 0xf1, 0x74, 0x12, 0x71, 0x48, 0x9b, - 0xf7, 0xcd, 0x16, 0xbe, 0x0f, 0x7f, 0xaf, 0x5e, 0xbe, 0xc7, 0x4e, 0xff, 0xde, 0x3f, 0x57, 0x64, - 0x12, 0x27, 0x57, 0xab, 0x17, 0xff, 0xe8, 0x57, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7a, 0x53, 0xb5, - 0xb8, 0xb0, 0x03, 0x00, 0x00, + // 568 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x54, 0x3f, 0x6f, 0x13, 0x4f, + 0x10, 0xf5, 0xfd, 0xa2, 0x1f, 0x24, 0x9b, 0x80, 0xc4, 0xc9, 0x85, 0x73, 0x98, 0x33, 0x18, 0x01, + 0x29, 0xc2, 0xae, 0x6c, 0x0a, 0x44, 0x24, 0x0a, 0x3b, 0x34, 0x10, 0x59, 0x42, 0x47, 0x81, 0x44, + 0x81, 0x75, 0xb7, 0xbb, 0xac, 0x57, 0xf8, 0x76, 0x4f, 0xbb, 0x7b, 0x47, 0xee, 0x1b, 0x50, 0x42, + 0x85, 0xe8, 0xf2, 0x01, 0x90, 0xf8, 0x1a, 0x94, 0x29, 0xa9, 0x10, 0xb2, 0x1b, 0x6a, 0x4a, 0x2a, + 0xe4, 0xfb, 0x63, 0x5f, 0x84, 0xb1, 0x2c, 0xa0, 0xdb, 0x99, 0x79, 0xfb, 0xde, 0x1b, 0xcd, 0x68, + 0xc0, 0x3e, 0x17, 0x86, 0x2a, 0x3c, 0xf2, 0xb9, 0x18, 0x6a, 0x8a, 0x63, 0xc5, 0x4d, 0x8a, 0x30, + 0x4e, 0x50, 0xa4, 0x64, 0xc2, 0x09, 0x55, 0x28, 0xe9, 0x20, 0x73, 0x0c, 0x23, 0x25, 0x8d, 0xb4, + 0xaf, 0x2f, 0x41, 0x43, 0x8c, 0x13, 0x58, 0xa2, 0x61, 0xd2, 0x71, 0x9a, 0x4c, 0x4a, 0x36, 0xa6, + 0xc8, 0x8f, 0x38, 0xf2, 0x85, 0x90, 0xc6, 0x37, 0x5c, 0x0a, 0x9d, 0x53, 0x38, 0x75, 0x26, 0x99, + 0xcc, 0x9e, 0x68, 0xf6, 0x2a, 0xb2, 0xbb, 0x58, 0xea, 0x50, 0xea, 0x61, 0x5e, 0xc8, 0x83, 0xb2, + 0x54, 0xd0, 0x65, 0x51, 0x10, 0xbf, 0x40, 0xbe, 0x48, 0x8b, 0x12, 0xe2, 0x01, 0x46, 0x63, 0xce, + 0x46, 0x06, 0x8f, 0x39, 0x15, 0x46, 0x23, 0x43, 0x05, 0xa1, 0x2a, 0xe4, 0xc2, 0x64, 0xbe, 0xe7, + 0x51, 0xfe, 0xa1, 0xfd, 0xce, 0x02, 0xf5, 0x81, 0x66, 0x3d, 0xad, 0x39, 0x13, 0x87, 0x52, 0xe8, + 0x38, 0xa4, 0xea, 0x88, 0xa6, 0xf6, 0x2e, 0xd8, 0xcc, 0xbb, 0xe2, 0xa4, 0x61, 0x5d, 0xb5, 0xf6, + 0xb6, 0xbc, 0xf3, 0x59, 0xfc, 0x90, 0xd8, 0x77, 0xc1, 0x85, 0xb2, 0xbb, 0xa1, 0x4f, 0x88, 0x6a, + 0xfc, 0x37, 0xab, 0xf7, 0xed, 0xef, 0x5f, 0x5a, 0x17, 0x53, 0x3f, 0x1c, 0x1f, 0xb4, 0x67, 0x59, + 0xaa, 0x75, 0xdb, 0xdb, 0x29, 0x81, 0x3d, 0x42, 0x94, 0x7d, 0x0d, 0xec, 0xe0, 0x42, 0x62, 0xf8, + 0x92, 0xa6, 0x8d, 0x8d, 0x8c, 0x77, 0x1b, 0x2f, 0x64, 0x0f, 0x36, 0x5f, 0x9f, 0xb4, 0x6a, 0xdf, + 0x4e, 0x5a, 0xb5, 0xb6, 0x0b, 0x9a, 0xcb, 0x8c, 0x79, 0x54, 0x47, 0x52, 0x68, 0xda, 0x7e, 0x0e, + 0xdc, 0x81, 0x66, 0x1e, 0x65, 0x5c, 0x1b, 0xaa, 0x4a, 0x84, 0x47, 0x5f, 0xf9, 0x8a, 0x3c, 0xa0, + 0x42, 0x86, 0x76, 0x1d, 0xfc, 0x4f, 0x66, 0x8f, 0xc2, 0x7f, 0x1e, 0xd8, 0x4d, 0xb0, 0x45, 0x68, + 0x24, 0x35, 0x37, 0xb2, 0x70, 0xee, 0x2d, 0x12, 0x15, 0xfd, 0x3d, 0x70, 0x73, 0x35, 0xff, 0xdc, + 0xc9, 0x7b, 0x0b, 0x5c, 0x19, 0x68, 0xf6, 0x24, 0x0e, 0x42, 0x6e, 0x4a, 0xe0, 0x80, 0xeb, 0x80, + 0x8e, 0xfc, 0x84, 0xcb, 0x58, 0xcd, 0x34, 0x75, 0x56, 0x35, 0x54, 0x15, 0x6e, 0x16, 0x09, 0xfb, + 0x31, 0xd8, 0x09, 0x2b, 0xe8, 0xcc, 0xd4, 0x76, 0x77, 0x1f, 0xf2, 0x00, 0xc3, 0xea, 0x2c, 0x61, + 0x65, 0x7a, 0x49, 0x07, 0x56, 0x15, 0xbc, 0x33, 0x0c, 0x95, 0x2e, 0x6e, 0x81, 0x1b, 0x2b, 0xad, + 0x95, 0x4d, 0x74, 0x7f, 0x6c, 0x80, 0x8d, 0x81, 0x66, 0xf6, 0x5b, 0x0b, 0x5c, 0xfa, 0x75, 0x1b, + 0xee, 0xc1, 0x35, 0xf6, 0x1c, 0x2e, 0x9b, 0x97, 0xd3, 0xfb, 0xe3, 0xaf, 0xa5, 0x37, 0xfb, 0xa3, + 0x05, 0x2e, 0xaf, 0x1a, 0xf4, 0xe1, 0xba, 0x12, 0x2b, 0x48, 0x9c, 0xa3, 0x7f, 0x40, 0x32, 0x77, + 0xfc, 0xc1, 0x02, 0xce, 0x8a, 0x7d, 0xe8, 0xaf, 0xab, 0xf5, 0x7b, 0x0e, 0xe7, 0xd1, 0xdf, 0x73, + 0x94, 0x76, 0xfb, 0x4f, 0x3f, 0x4d, 0x5c, 0xeb, 0x74, 0xe2, 0x5a, 0x5f, 0x27, 0xae, 0xf5, 0x66, + 0xea, 0xd6, 0x4e, 0xa7, 0x6e, 0xed, 0xf3, 0xd4, 0xad, 0x3d, 0xbb, 0xcf, 0xb8, 0x19, 0xc5, 0x01, + 0xc4, 0x32, 0x2c, 0x8e, 0x10, 0x5a, 0xc8, 0xde, 0x9e, 0xdf, 0xc7, 0xa4, 0x8b, 0x8e, 0xcf, 0x1e, + 0x49, 0x93, 0x46, 0x54, 0x07, 0xe7, 0xb2, 0x2b, 0x73, 0xe7, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xe3, 0x4f, 0x57, 0x26, 0x55, 0x05, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -249,6 +338,7 @@ const _ = grpc.SupportPackageIsVersion4 type MsgClient interface { AssignConsumerKey(ctx context.Context, in *MsgAssignConsumerKey, opts ...grpc.CallOption) (*MsgAssignConsumerKeyResponse, error) RegisterConsumerRewardDenom(ctx context.Context, in *MsgRegisterConsumerRewardDenom, opts ...grpc.CallOption) (*MsgRegisterConsumerRewardDenomResponse, error) + SubmitConsumerMisbehaviour(ctx context.Context, in *MsgSubmitConsumerMisbehaviour, opts ...grpc.CallOption) (*MsgSubmitConsumerMisbehaviourResponse, error) } type msgClient struct { @@ -277,10 +367,20 @@ func (c *msgClient) RegisterConsumerRewardDenom(ctx context.Context, in *MsgRegi return out, nil } +func (c *msgClient) SubmitConsumerMisbehaviour(ctx context.Context, in *MsgSubmitConsumerMisbehaviour, opts ...grpc.CallOption) (*MsgSubmitConsumerMisbehaviourResponse, error) { + out := new(MsgSubmitConsumerMisbehaviourResponse) + err := c.cc.Invoke(ctx, "/interchain_security.ccv.provider.v1.Msg/SubmitConsumerMisbehaviour", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // MsgServer is the server API for Msg service. type MsgServer interface { AssignConsumerKey(context.Context, *MsgAssignConsumerKey) (*MsgAssignConsumerKeyResponse, error) RegisterConsumerRewardDenom(context.Context, *MsgRegisterConsumerRewardDenom) (*MsgRegisterConsumerRewardDenomResponse, error) + SubmitConsumerMisbehaviour(context.Context, *MsgSubmitConsumerMisbehaviour) (*MsgSubmitConsumerMisbehaviourResponse, error) } // UnimplementedMsgServer can be embedded to have forward compatible implementations. @@ -293,6 +393,9 @@ func (*UnimplementedMsgServer) AssignConsumerKey(ctx context.Context, req *MsgAs func (*UnimplementedMsgServer) RegisterConsumerRewardDenom(ctx context.Context, req *MsgRegisterConsumerRewardDenom) (*MsgRegisterConsumerRewardDenomResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RegisterConsumerRewardDenom not implemented") } +func (*UnimplementedMsgServer) SubmitConsumerMisbehaviour(ctx context.Context, req *MsgSubmitConsumerMisbehaviour) (*MsgSubmitConsumerMisbehaviourResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SubmitConsumerMisbehaviour not implemented") +} func RegisterMsgServer(s grpc1.Server, srv MsgServer) { s.RegisterService(&_Msg_serviceDesc, srv) @@ -334,6 +437,24 @@ func _Msg_RegisterConsumerRewardDenom_Handler(srv interface{}, ctx context.Conte return interceptor(ctx, in, info, handler) } +func _Msg_SubmitConsumerMisbehaviour_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgSubmitConsumerMisbehaviour) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).SubmitConsumerMisbehaviour(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/interchain_security.ccv.provider.v1.Msg/SubmitConsumerMisbehaviour", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).SubmitConsumerMisbehaviour(ctx, req.(*MsgSubmitConsumerMisbehaviour)) + } + return interceptor(ctx, in, info, handler) +} + var _Msg_serviceDesc = grpc.ServiceDesc{ ServiceName: "interchain_security.ccv.provider.v1.Msg", HandlerType: (*MsgServer)(nil), @@ -346,6 +467,10 @@ var _Msg_serviceDesc = grpc.ServiceDesc{ MethodName: "RegisterConsumerRewardDenom", Handler: _Msg_RegisterConsumerRewardDenom_Handler, }, + { + MethodName: "SubmitConsumerMisbehaviour", + Handler: _Msg_SubmitConsumerMisbehaviour_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "interchain_security/ccv/provider/v1/tx.proto", @@ -478,6 +603,71 @@ func (m *MsgRegisterConsumerRewardDenomResponse) MarshalToSizedBuffer(dAtA []byt return len(dAtA) - i, nil } +func (m *MsgSubmitConsumerMisbehaviour) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgSubmitConsumerMisbehaviour) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgSubmitConsumerMisbehaviour) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Misbehaviour != nil { + { + size, err := m.Misbehaviour.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if len(m.Submitter) > 0 { + i -= len(m.Submitter) + copy(dAtA[i:], m.Submitter) + i = encodeVarintTx(dAtA, i, uint64(len(m.Submitter))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MsgSubmitConsumerMisbehaviourResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgSubmitConsumerMisbehaviourResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgSubmitConsumerMisbehaviourResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + func encodeVarintTx(dAtA []byte, offset int, v uint64) int { offset -= sovTx(v) base := offset @@ -545,6 +735,32 @@ func (m *MsgRegisterConsumerRewardDenomResponse) Size() (n int) { return n } +func (m *MsgSubmitConsumerMisbehaviour) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Submitter) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if m.Misbehaviour != nil { + l = m.Misbehaviour.Size() + n += 1 + l + sovTx(uint64(l)) + } + return n +} + +func (m *MsgSubmitConsumerMisbehaviourResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + func sovTx(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -911,6 +1127,174 @@ func (m *MsgRegisterConsumerRewardDenomResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *MsgSubmitConsumerMisbehaviour) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgSubmitConsumerMisbehaviour: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgSubmitConsumerMisbehaviour: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Submitter", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Submitter = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Misbehaviour", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Misbehaviour == nil { + m.Misbehaviour = &types.Misbehaviour{} + } + if err := m.Misbehaviour.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgSubmitConsumerMisbehaviourResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgSubmitConsumerMisbehaviourResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgSubmitConsumerMisbehaviourResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipTx(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/ccv/types/events.go b/x/ccv/types/events.go index ba71e063f3..df91333fa0 100644 --- a/x/ccv/types/events.go +++ b/x/ccv/types/events.go @@ -9,11 +9,11 @@ const ( EventTypeConsumerClientCreated = "consumer_client_created" EventTypeAssignConsumerKey = "assign_consumer_key" EventTypeRegisterConsumerRewardDenom = "register_consumer_reward_denom" - - EventTypeExecuteConsumerChainSlash = "execute_consumer_chain_slash" - EventTypeFeeDistribution = "fee_distribution" - EventTypeConsumerSlashRequest = "consumer_slash_request" - EventTypeVSCMatured = "vsc_matured" + EventTypeSubmitConsumerMisbehaviour = "submit_consumer_misbehaviour" + EventTypeExecuteConsumerChainSlash = "execute_consumer_chain_slash" + EventTypeFeeDistribution = "fee_distribution" + EventTypeConsumerSlashRequest = "consumer_slash_request" + EventTypeVSCMatured = "vsc_matured" AttributeKeyAckSuccess = "success" AttributeKeyAck = "acknowledgement" @@ -33,6 +33,11 @@ const ( AttributeUnbondingPeriod = "unbonding_period" AttributeProviderValidatorAddress = "provider_validator_address" AttributeConsumerConsensusPubKey = "consumer_consensus_pub_key" + AttributeSubmitterAddress = "submitter_address" + AttributeConsumerMisbehaviour = "consumer_misbehaviour" + AttributeMisbehaviourClientId = "misbehaviour_client_id" + AttributeMisbehaviourHeight1 = "misbehaviour_height_1" + AttributeMisbehaviourHeight2 = "misbehaviour_height_2" AttributeDistributionCurrentHeight = "current_distribution_height" AttributeDistributionNextHeight = "next_distribution_height" diff --git a/x/ccv/types/expected_keepers.go b/x/ccv/types/expected_keepers.go index 7f18324c8d..b4dea9b670 100644 --- a/x/ccv/types/expected_keepers.go +++ b/x/ccv/types/expected_keepers.go @@ -86,6 +86,10 @@ type ClientKeeper interface { GetClientState(ctx sdk.Context, clientID string) (ibcexported.ClientState, bool) GetLatestClientConsensusState(ctx sdk.Context, clientID string) (ibcexported.ConsensusState, bool) GetSelfConsensusState(ctx sdk.Context, height ibcexported.Height) (ibcexported.ConsensusState, error) + ClientStore(ctx sdk.Context, clientID string) sdk.KVStore + SetClientState(ctx sdk.Context, clientID string, clientState ibcexported.ClientState) + GetClientConsensusState(ctx sdk.Context, clientID string, height ibcexported.Height) (ibcexported.ConsensusState, bool) + CheckMisbehaviourAndUpdateState(ctx sdk.Context, misbehaviour ibcexported.Misbehaviour) error } // DistributionKeeper defines the expected interface of the distribution keeper From 21e3d839ce3fc7141b6c808ab40b46899d5f9123 Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Wed, 23 Aug 2023 16:38:35 +0200 Subject: [PATCH 02/12] feat: improve ICS misbehaviour E2E testing coverage (#1225) * update e2e tests * update the chain halt assertion --- tests/e2e/actions_consumer_misbehaviour.go | 24 ++++++++++++++++++++-- tests/e2e/main.go | 2 ++ tests/e2e/steps_consumer_misbehaviour.go | 14 +++++++------ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/tests/e2e/actions_consumer_misbehaviour.go b/tests/e2e/actions_consumer_misbehaviour.go index 2b01c2818e..84eb93152c 100644 --- a/tests/e2e/actions_consumer_misbehaviour.go +++ b/tests/e2e/actions_consumer_misbehaviour.go @@ -29,7 +29,7 @@ func (tr TestRun) forkConsumerChain(action forkConsumerChainAction, verbose bool ) if verbose { - fmt.Println("forkConsumerChain - reconfigure node cmd:", configureNodeCmd.String()) + log.Println("forkConsumerChain - reconfigure node cmd:", configureNodeCmd.String()) } cmdReader, err := configureNodeCmd.StdoutPipe() @@ -47,7 +47,7 @@ func (tr TestRun) forkConsumerChain(action forkConsumerChainAction, verbose bool for scanner.Scan() { out := scanner.Text() if verbose { - fmt.Println("fork consumer validator : " + out) + log.Println("fork consumer validator : " + out) } if out == done { break @@ -90,3 +90,23 @@ func (tr TestRun) updateLightClient( tr.waitBlocks(action.hostChain, 5, 30*time.Second) } + +type assertChainIsHaltedAction struct { + chain chainID +} + +// assertChainIsHalted verifies that the chain isn't producing blocks +// by checking that the block height is still the same after 20 seconds +func (tr TestRun) assertChainIsHalted( + action assertChainIsHaltedAction, + verbose bool, +) { + blockHeight := tr.getBlockHeight(action.chain) + time.Sleep(20 * time.Second) + if blockHeight != tr.getBlockHeight(action.chain) { + panic(fmt.Sprintf("chain %v isn't expected to produce blocks", action.chain)) + } + if verbose { + log.Printf("assertChainIsHalted - chain %v was successfully halted\n", action.chain) + } +} diff --git a/tests/e2e/main.go b/tests/e2e/main.go index 4c7b6722cd..406a015e63 100644 --- a/tests/e2e/main.go +++ b/tests/e2e/main.go @@ -178,6 +178,8 @@ func (tr *TestRun) runStep(step Step, verbose bool) { tr.forkConsumerChain(action, verbose) case updateLightClientAction: tr.updateLightClient(action, verbose) + case assertChainIsHaltedAction: + tr.assertChainIsHalted(action, verbose) default: log.Fatalf("unknown action in testRun %s: %#v", tr.name, action) } diff --git a/tests/e2e/steps_consumer_misbehaviour.go b/tests/e2e/steps_consumer_misbehaviour.go index 7d7fda94b1..6401b5f638 100644 --- a/tests/e2e/steps_consumer_misbehaviour.go +++ b/tests/e2e/steps_consumer_misbehaviour.go @@ -241,13 +241,15 @@ func stepsCauseConsumerMisbehaviour(consumerName string) []Step { }, }, }, - chainID(consumerName): ChainState{ - ValPowers: &map[validatorID]uint{ - validatorID("alice"): 511, - validatorID("bob"): 20, - }, - }, }, }, + // we expect the consumer chain to be halted since the last VSC packet should + // have updated the alice validator power to 0. + { + action: assertChainIsHaltedAction{ + chain: chainID("consu"), + }, + state: State{}, + }, } } From 292ad7539b889ba3ac98165c5da6124da601ae30 Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Wed, 23 Aug 2023 18:27:47 +0200 Subject: [PATCH 03/12] refactor: address comments of ICS Misbehaviour PRs #826 and #1148 (#1223) * remove interface * improve comment * update godoc * address last comments --- x/ccv/provider/keeper/misbehaviour.go | 12 +++--------- x/ccv/provider/types/codec.go | 5 ----- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 4d8f517e95..0438df36e3 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -1,8 +1,6 @@ package keeper import ( - "sort" - "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -109,7 +107,6 @@ func (k Keeper) GetByzantineValidators(ctx sdk.Context, misbehaviour ibctmtypes. } } - sort.Sort(tmtypes.ValidatorsByVotingPower(validators)) return validators, nil } @@ -134,10 +131,6 @@ func headerToLightBlock(h ibctmtypes.Header) (*tmtypes.LightBlock, error) { // CheckMisbehaviour checks that headers in the given misbehaviour forms // a valid light client attack and that the corresponding light client isn't expired func (k Keeper) CheckMisbehaviour(ctx sdk.Context, misbehaviour ibctmtypes.Misbehaviour) error { - if err := misbehaviour.ValidateBasic(); err != nil { - return err - } - clientState, found := k.clientKeeper.GetClientState(ctx, misbehaviour.GetClientID()) if !found { return sdkerrors.Wrapf(ibcclienttypes.ErrClientNotFound, "cannot check misbehaviour for client with ID %s", misbehaviour.GetClientID()) @@ -147,14 +140,15 @@ func (k Keeper) CheckMisbehaviour(ctx sdk.Context, misbehaviour ibctmtypes.Misbe // Check that the headers are at the same height to ensure that // the misbehaviour is for a light client attack and not a time violation, - // see https://github.com/cosmos/ibc-go/blob/8f53c21361f9d65448a850c2eafcf3ab3c384a61/modules/light-clients/07-tendermint/types/misbehaviour_handle.go#L56 + // https://github.com/cosmos/ibc-go/blob/v4.2.0/modules/light-clients/07-tendermint/types/misbehaviour_handle.go#L53-L58 if !misbehaviour.Header1.GetHeight().EQ(misbehaviour.Header2.GetHeight()) { return sdkerrors.Wrap(ibcclienttypes.ErrInvalidMisbehaviour, "headers are not at same height") } // CheckMisbehaviourAndUpdateState verifies the misbehaviour against the trusted consensus states // but does NOT update the light client state. - // Note CheckMisbehaviourAndUpdateState returns an error if the trusted consensus states are expired + // Note that the CometBFT CheckMisbehaviourAndUpdateState method returns an error if the trusted consensus states are expired, + // see https://github.com/cosmos/ibc-go/blob/v4.2.0/modules/light-clients/07-tendermint/types/misbehaviour_handle.go#L120 _, err := clientState.CheckMisbehaviourAndUpdateState(ctx, k.cdc, clientStore, &misbehaviour) if err != nil { return err diff --git a/x/ccv/provider/types/codec.go b/x/ccv/provider/types/codec.go index 2f28b398f5..0ef3c2d296 100644 --- a/x/ccv/provider/types/codec.go +++ b/x/ccv/provider/types/codec.go @@ -6,7 +6,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/msgservice" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" - "github.com/cosmos/ibc-go/v4/modules/core/exported" ) // RegisterLegacyAminoCodec registers the necessary x/ibc transfer interfaces and concrete types @@ -41,10 +40,6 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) { (*sdk.Msg)(nil), &MsgSubmitConsumerMisbehaviour{}, ) - registry.RegisterInterface( - "ibc.core.client.v1.Misbehaviour", - (*exported.Misbehaviour)(nil), - ) msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) } From f168b9b4a5d15f3641d9cb09182b923f7688aead Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Mon, 28 Aug 2023 10:17:02 +0200 Subject: [PATCH 04/12] feat: add handler for consumer double voting (#1232) * create new endpoint for consumer double voting * add first draft handling logic * first iteration of double voting * draft first mem test * error handling * refactor * add unit test of double voting verification * remove evidence age checks * document * doc * protogen * reformat double voting handling * logger nit * nits * check evidence age duration * move verify double voting evidence to ut * fix nit * nits * fix e2e tests * improve double vote testing coverage * remove TODO * lint * add UT for JailAndTombstoneValidator * nits * nits * remove tombstoning and evidence age check * lint * typo * improve godoc --- .../ccv/provider/v1/tx.proto | 19 + tests/integration/double_vote.go | 123 +++++ tests/integration/misbehaviour.go | 3 +- testutil/crypto/evidence.go | 56 ++ testutil/integration/debug_test.go | 10 +- .../proto/tendermint/types/evidence.proto | 38 ++ x/ccv/provider/client/cli/tx.go | 46 ++ x/ccv/provider/handler.go | 3 + x/ccv/provider/keeper/double_vote.go | 109 ++++ x/ccv/provider/keeper/double_vote_test.go | 284 ++++++++++ x/ccv/provider/keeper/misbehaviour.go | 35 +- x/ccv/provider/keeper/msg_server.go | 25 + x/ccv/provider/keeper/punish_validator.go | 38 ++ .../provider/keeper/punish_validator_test.go | 132 +++++ x/ccv/provider/types/msg.go | 47 ++ x/ccv/provider/types/tx.pb.go | 512 ++++++++++++++++-- x/ccv/types/errors.go | 1 + x/ccv/types/events.go | 1 + 18 files changed, 1417 insertions(+), 65 deletions(-) create mode 100644 tests/integration/double_vote.go create mode 100644 testutil/crypto/evidence.go create mode 100644 third_party/proto/tendermint/types/evidence.proto create mode 100644 x/ccv/provider/keeper/double_vote.go create mode 100644 x/ccv/provider/keeper/double_vote_test.go create mode 100644 x/ccv/provider/keeper/punish_validator.go create mode 100644 x/ccv/provider/keeper/punish_validator_test.go diff --git a/proto/interchain_security/ccv/provider/v1/tx.proto b/proto/interchain_security/ccv/provider/v1/tx.proto index 61be3064ea..64e4c88b70 100644 --- a/proto/interchain_security/ccv/provider/v1/tx.proto +++ b/proto/interchain_security/ccv/provider/v1/tx.proto @@ -8,12 +8,15 @@ import "gogoproto/gogo.proto"; import "cosmos_proto/cosmos.proto"; import "google/protobuf/any.proto"; import "ibc/lightclients/tendermint/v1/tendermint.proto"; +import "tendermint/types/evidence.proto"; + // Msg defines the Msg service. service Msg { rpc AssignConsumerKey(MsgAssignConsumerKey) returns (MsgAssignConsumerKeyResponse); rpc RegisterConsumerRewardDenom(MsgRegisterConsumerRewardDenom) returns (MsgRegisterConsumerRewardDenomResponse); rpc SubmitConsumerMisbehaviour(MsgSubmitConsumerMisbehaviour) returns (MsgSubmitConsumerMisbehaviourResponse); + rpc SubmitConsumerDoubleVoting(MsgSubmitConsumerDoubleVoting) returns (MsgSubmitConsumerDoubleVotingResponse); } message MsgAssignConsumerKey { @@ -59,3 +62,19 @@ message MsgSubmitConsumerMisbehaviour { } message MsgSubmitConsumerMisbehaviourResponse {} + + +// MsgSubmitConsumerDoubleVoting defines a message that reports an equivocation +// observed on a consumer chain +message MsgSubmitConsumerDoubleVoting { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + string submitter = 1; + // The equivocation of the consumer chain wrapping + // an evidence of a validator that signed two conflicting votes + tendermint.types.DuplicateVoteEvidence duplicate_vote_evidence = 2; + // The light client header of the infraction block + ibc.lightclients.tendermint.v1.Header infraction_block_header = 3; +} + +message MsgSubmitConsumerDoubleVotingResponse {} \ No newline at end of file diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go new file mode 100644 index 0000000000..04d976ed61 --- /dev/null +++ b/tests/integration/double_vote.go @@ -0,0 +1,123 @@ +package integration + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + testutil "github.com/cosmos/interchain-security/v2/testutil/crypto" + "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" + tmtypes "github.com/tendermint/tendermint/types" +) + +// TestHandleConsumerDoubleVoting verifies that handling a double voting evidence +// of a consumer chain results in the expected jailing of the malicious validator +func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { + s.SetupCCVChannel(s.path) + // required to have the consumer client revision height greater than 0 + s.SendEmptyVSCPacket() + + // create signing info for all validators + for _, v := range s.providerChain.Vals.Validators { + s.setDefaultValSigningInfo(*v) + } + + valSet, err := tmtypes.ValidatorSetFromProto(s.consumerChain.LastHeader.ValidatorSet) + s.Require().NoError(err) + + val := valSet.Validators[0] + signer := s.consumerChain.Signers[val.Address.String()] + + blockID1 := testutil.MakeBlockID([]byte("blockhash"), 1000, []byte("partshash")) + blockID2 := testutil.MakeBlockID([]byte("blockhash2"), 1000, []byte("partshash")) + + // Note that votes are signed along with the chain ID + // see VoteSignBytes in https://github.com/cometbft/cometbft/blob/main/types/vote.go#L139 + vote1 := testutil.MakeAndSignVote( + blockID1, + s.consumerCtx().BlockHeight(), + s.consumerCtx().BlockTime(), + valSet, + signer, + s.consumerChain.ChainID, + ) + + badVote := testutil.MakeAndSignVote( + blockID2, + s.consumerCtx().BlockHeight(), + s.consumerCtx().BlockTime(), + valSet, + signer, + s.consumerChain.ChainID, + ) + + testCases := []struct { + name string + ev *tmtypes.DuplicateVoteEvidence + chainID string + expPass bool + }{ + { + "invalid consumer chain id - shouldn't pass", + &tmtypes.DuplicateVoteEvidence{ + VoteA: vote1, + VoteB: badVote, + ValidatorPower: val.VotingPower, + TotalVotingPower: val.VotingPower, + Timestamp: s.consumerCtx().BlockTime(), + }, + "chainID", + false, + }, + { + // create an invalid evidence containing two identical votes + "invalid double voting evidence - shouldn't pass", + &tmtypes.DuplicateVoteEvidence{ + VoteA: vote1, + VoteB: vote1, + ValidatorPower: val.VotingPower, + TotalVotingPower: val.VotingPower, + Timestamp: s.consumerCtx().BlockTime(), + }, + s.consumerChain.ChainID, + false, + }, + { + // In order to create an evidence for a consumer chain, + // we create two votes that only differ by their Block IDs and + // signed them using the same validator private key and chain ID + // of the consumer chain + "valid double voting evidence - should pass", + &tmtypes.DuplicateVoteEvidence{ + VoteA: vote1, + VoteB: badVote, + ValidatorPower: val.VotingPower, + TotalVotingPower: val.VotingPower, + Timestamp: s.consumerCtx().BlockTime(), + }, + s.consumerChain.ChainID, + true, + }, + } + + consuAddr := types.NewConsumerConsAddress(sdk.ConsAddress(val.Address.Bytes())) + provAddr := s.providerApp.GetProviderKeeper().GetProviderAddrFromConsumerAddr(s.providerCtx(), s.consumerChain.ChainID, consuAddr) + + for _, tc := range testCases { + s.Run(tc.name, func() { + err = s.providerApp.GetProviderKeeper().HandleConsumerDoubleVoting( + s.providerCtx(), + tc.ev, + tc.chainID, + ) + if tc.expPass { + s.Require().NoError(err) + + // verifies that the jailing has occurred + s.Require().True(s.providerApp.GetTestStakingKeeper().IsValidatorJailed(s.providerCtx(), provAddr.ToSdkConsAddr())) + } else { + s.Require().Error(err) + + // verifies that no jailing and has occurred + s.Require().False(s.providerApp.GetTestStakingKeeper().IsValidatorJailed(s.providerCtx(), provAddr.ToSdkConsAddr())) + } + }) + } +} diff --git a/tests/integration/misbehaviour.go b/tests/integration/misbehaviour.go index c3b590d5c1..f7cebc84c0 100644 --- a/tests/integration/misbehaviour.go +++ b/tests/integration/misbehaviour.go @@ -11,7 +11,7 @@ import ( ) // TestHandleConsumerMisbehaviour tests that handling a valid misbehaviour, -// with conflicting headers forming an equivocation, results in the jailing and tombstoning of the validators +// with conflicting headers forming an equivocation, results in the jailing of the validators func (s *CCVTestSuite) TestHandleConsumerMisbehaviour() { s.SetupCCVChannel(s.path) // required to have the consumer client revision height greater than 0 @@ -63,7 +63,6 @@ func (s *CCVTestSuite) TestHandleConsumerMisbehaviour() { val, ok := s.providerApp.GetTestStakingKeeper().GetValidatorByConsAddr(s.providerCtx(), provAddr.Address) s.Require().True(ok) s.Require().True(val.Jailed) - s.Require().True(s.providerApp.GetTestSlashingKeeper().IsTombstoned(s.providerCtx(), provAddr.Address)) } } diff --git a/testutil/crypto/evidence.go b/testutil/crypto/evidence.go new file mode 100644 index 0000000000..050c17331a --- /dev/null +++ b/testutil/crypto/evidence.go @@ -0,0 +1,56 @@ +package crypto + +import ( + "time" + + "github.com/tendermint/tendermint/crypto/tmhash" + tmtypes "github.com/tendermint/tendermint/types" +) + +// utility function duplicated from CometBFT +// see https://github.com/cometbft/cometbft/blob/main/evidence/verify_test.go#L554 +func MakeBlockID(hash []byte, partSetSize uint32, partSetHash []byte) tmtypes.BlockID { + var ( + h = make([]byte, tmhash.Size) + psH = make([]byte, tmhash.Size) + ) + copy(h, hash) + copy(psH, partSetHash) + return tmtypes.BlockID{ + Hash: h, + PartSetHeader: tmtypes.PartSetHeader{ + Total: partSetSize, + Hash: psH, + }, + } +} + +func MakeAndSignVote( + blockID tmtypes.BlockID, + blockHeight int64, + blockTime time.Time, + valSet *tmtypes.ValidatorSet, + signer tmtypes.PrivValidator, + chainID string, +) *tmtypes.Vote { + vote, err := tmtypes.MakeVote( + blockHeight, + blockID, + valSet, + signer, + chainID, + blockTime, + ) + if err != nil { + panic(err) + } + + v := vote.ToProto() + err = signer.SignVote(chainID, v) + if err != nil { + panic(err) + } + + vote.Signature = v.Signature + return vote +} diff --git a/testutil/integration/debug_test.go b/testutil/integration/debug_test.go index 2d9421300b..fcb19d5255 100644 --- a/testutil/integration/debug_test.go +++ b/testutil/integration/debug_test.go @@ -258,7 +258,7 @@ func TestRecycleTransferChannel(t *testing.T) { } // -// Misbehaviour test +// Misbehaviour tests // func TestHandleConsumerMisbehaviour(t *testing.T) { @@ -272,3 +272,11 @@ func TestGetByzantineValidators(t *testing.T) { func TestCheckMisbehaviour(t *testing.T) { runCCVTestByName(t, "TestCheckMisbehaviour") } + +// +// Equivocation test +// + +func TestHandleConsumerDoubleVoting(t *testing.T) { + runCCVTestByName(t, "TestHandleConsumerDoubleVoting") +} diff --git a/third_party/proto/tendermint/types/evidence.proto b/third_party/proto/tendermint/types/evidence.proto new file mode 100644 index 0000000000..451b8dca3c --- /dev/null +++ b/third_party/proto/tendermint/types/evidence.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; +package tendermint.types; + +option go_package = "github.com/tendermint/tendermint/proto/tendermint/types"; + +import "gogoproto/gogo.proto"; +import "google/protobuf/timestamp.proto"; +import "tendermint/types/types.proto"; +import "tendermint/types/validator.proto"; + +message Evidence { + oneof sum { + DuplicateVoteEvidence duplicate_vote_evidence = 1; + LightClientAttackEvidence light_client_attack_evidence = 2; + } +} + +// DuplicateVoteEvidence contains evidence of a validator signed two conflicting votes. +message DuplicateVoteEvidence { + tendermint.types.Vote vote_a = 1; + tendermint.types.Vote vote_b = 2; + int64 total_voting_power = 3; + int64 validator_power = 4; + google.protobuf.Timestamp timestamp = 5 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true]; +} + +// LightClientAttackEvidence contains evidence of a set of validators attempting to mislead a light client. +message LightClientAttackEvidence { + tendermint.types.LightBlock conflicting_block = 1; + int64 common_height = 2; + repeated tendermint.types.Validator byzantine_validators = 3; + int64 total_voting_power = 4; + google.protobuf.Timestamp timestamp = 5 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true]; +} + +message EvidenceList { + repeated Evidence evidence = 1 [(gogoproto.nullable) = false]; +} diff --git a/x/ccv/provider/client/cli/tx.go b/x/ccv/provider/client/cli/tx.go index 1cad8b38ce..1d3ccc0011 100644 --- a/x/ccv/provider/client/cli/tx.go +++ b/x/ccv/provider/client/cli/tx.go @@ -11,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/version" ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" @@ -29,6 +30,7 @@ func GetTxCmd() *cobra.Command { cmd.AddCommand(NewAssignConsumerKeyCmd()) cmd.AddCommand(NewRegisterConsumerRewardDenomCmd()) cmd.AddCommand(NewSubmitConsumerMisbehaviourCmd()) + cmd.AddCommand(NewSubmitConsumerDoubleVotingCmd()) return cmd } @@ -148,3 +150,47 @@ Examples: return cmd } + +func NewSubmitConsumerDoubleVotingCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "submit consumer-double-voting [evidence] [infraction_header]", + Short: "submit a double voting evidence for a consumer chain", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + txf := tx.NewFactoryCLI(clientCtx, cmd.Flags()). + WithTxConfig(clientCtx.TxConfig).WithAccountRetriever(clientCtx.AccountRetriever) + + submitter := clientCtx.GetFromAddress() + var ev *tmproto.DuplicateVoteEvidence + if err := clientCtx.Codec.UnmarshalInterfaceJSON([]byte(args[1]), &ev); err != nil { + return err + } + + var header ibctmtypes.Header + if err := clientCtx.Codec.UnmarshalInterfaceJSON([]byte(args[2]), &header); err != nil { + return err + } + + msg, err := types.NewMsgSubmitConsumerDoubleVoting(submitter, ev, nil) + if err != nil { + return err + } + if err := msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txf, msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + _ = cmd.MarkFlagRequired(flags.FlagFrom) + + return cmd +} diff --git a/x/ccv/provider/handler.go b/x/ccv/provider/handler.go index 6fa38b5ddf..8dca550732 100644 --- a/x/ccv/provider/handler.go +++ b/x/ccv/provider/handler.go @@ -24,6 +24,9 @@ func NewHandler(k *keeper.Keeper) sdk.Handler { case *types.MsgSubmitConsumerMisbehaviour: res, err := msgServer.SubmitConsumerMisbehaviour(sdk.WrapSDKContext(ctx), msg) return sdk.WrapServiceResult(ctx, res, err) + case *types.MsgSubmitConsumerDoubleVoting: + res, err := msgServer.SubmitConsumerDoubleVoting(sdk.WrapSDKContext(ctx), msg) + return sdk.WrapServiceResult(ctx, res, err) default: return nil, errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", types.ModuleName, msg) } diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go new file mode 100644 index 0000000000..ee47e233ee --- /dev/null +++ b/x/ccv/provider/keeper/double_vote.go @@ -0,0 +1,109 @@ +package keeper + +import ( + "bytes" + "fmt" + + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" + ccvtypes "github.com/cosmos/interchain-security/v2/x/ccv/types" + tmprotocrypto "github.com/tendermint/tendermint/proto/tendermint/crypto" + tmtypes "github.com/tendermint/tendermint/types" +) + +// HandleConsumerDoubleVoting verifies a double voting evidence for a given a consumer chain and, +// if successful, executes the jailing of the malicious validator. +func (k Keeper) HandleConsumerDoubleVoting(ctx sdk.Context, evidence *tmtypes.DuplicateVoteEvidence, chainID string) error { + // get the validator's consensus address on the provider + providerAddr := k.GetProviderAddrFromConsumerAddr( + ctx, + chainID, + types.NewConsumerConsAddress(sdk.ConsAddress(evidence.VoteA.ValidatorAddress.Bytes())), + ) + + // get the consumer chain public key assigned to the validator + consuPubkey, ok := k.GetValidatorConsumerPubKey(ctx, chainID, providerAddr) + if !ok { + return fmt.Errorf("cannot find public key for validator %s and consumer chain %s", providerAddr.String(), chainID) + } + + // verifies the double voting evidence using the consumer chain public key + if err := k.VerifyDoubleVotingEvidence(ctx, *evidence, chainID, consuPubkey); err != nil { + return err + } + + // execute the jailing + k.JailValidator(ctx, providerAddr) + + k.Logger(ctx).Info( + "confirmed equivocation", + "byzantine validator address", providerAddr, + ) + + return nil +} + +// VerifyDoubleVotingEvidence verifies a double voting evidence +// for a given chain id and a validator public key +func (k Keeper) VerifyDoubleVotingEvidence( + ctx sdk.Context, + evidence tmtypes.DuplicateVoteEvidence, + chainID string, + pubkey tmprotocrypto.PublicKey, +) error { + // Note that since we're only jailing validators for double voting on a consumer chain, + // the age of the evidence is irrelevant and therefore isn't checked. + + // TODO: check the age of the evidence once we slash + // validators for double voting on a consumer chain + + // H/R/S must be the same + if evidence.VoteA.Height != evidence.VoteB.Height || + evidence.VoteA.Round != evidence.VoteB.Round || + evidence.VoteA.Type != evidence.VoteB.Type { + return sdkerrors.Wrapf( + ccvtypes.ErrInvalidEvidence, + "h/r/s does not match: %d/%d/%v vs %d/%d/%v", + evidence.VoteA.Height, evidence.VoteA.Round, evidence.VoteA.Type, + evidence.VoteB.Height, evidence.VoteB.Round, evidence.VoteB.Type) + } + + // Address must be the same + if !bytes.Equal(evidence.VoteA.ValidatorAddress, evidence.VoteB.ValidatorAddress) { + return sdkerrors.Wrapf( + ccvtypes.ErrInvalidEvidence, + "validator addresses do not match: %X vs %X", + evidence.VoteA.ValidatorAddress, + evidence.VoteB.ValidatorAddress, + ) + } + + // BlockIDs must be different + if evidence.VoteA.BlockID.Equals(evidence.VoteB.BlockID) { + return sdkerrors.Wrapf( + ccvtypes.ErrInvalidEvidence, + "block IDs are the same (%v) - not a real duplicate vote", + evidence.VoteA.BlockID, + ) + } + + pk, err := cryptocodec.FromTmProtoPublicKey(pubkey) + if err != nil { + return err + } + + va := evidence.VoteA.ToProto() + vb := evidence.VoteB.ToProto() + + // signatures must be valid + if !pk.VerifySignature(tmtypes.VoteSignBytes(chainID, va), evidence.VoteA.Signature) { + return fmt.Errorf("verifying VoteA: %w", tmtypes.ErrVoteInvalidSignature) + } + if !pk.VerifySignature(tmtypes.VoteSignBytes(chainID, vb), evidence.VoteB.Signature) { + return fmt.Errorf("verifying VoteB: %w", tmtypes.ErrVoteInvalidSignature) + } + + return nil +} diff --git a/x/ccv/provider/keeper/double_vote_test.go b/x/ccv/provider/keeper/double_vote_test.go new file mode 100644 index 0000000000..9bb789c381 --- /dev/null +++ b/x/ccv/provider/keeper/double_vote_test.go @@ -0,0 +1,284 @@ +package keeper_test + +import ( + "testing" + "time" + + testutil "github.com/cosmos/interchain-security/v2/testutil/crypto" + testkeeper "github.com/cosmos/interchain-security/v2/testutil/keeper" + "github.com/stretchr/testify/require" + tmencoding "github.com/tendermint/tendermint/crypto/encoding" + tmprotocrypto "github.com/tendermint/tendermint/proto/tendermint/crypto" + tmtypes "github.com/tendermint/tendermint/types" +) + +func TestVerifyDoubleVotingEvidence(t *testing.T) { + keeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + chainID := "consumer" + + signer1 := tmtypes.NewMockPV() + signer2 := tmtypes.NewMockPV() + + val1 := tmtypes.NewValidator(signer1.PrivKey.PubKey(), 1) + val2 := tmtypes.NewValidator(signer2.PrivKey.PubKey(), 1) + + valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{val1, val2}) + + blockID1 := testutil.MakeBlockID([]byte("blockhash"), 1000, []byte("partshash")) + blockID2 := testutil.MakeBlockID([]byte("blockhash2"), 1000, []byte("partshash")) + + ctx = ctx.WithBlockTime(time.Now()) + + valPubkey1, err := tmencoding.PubKeyToProto(val1.PubKey) + require.NoError(t, err) + + valPubkey2, err := tmencoding.PubKeyToProto(val2.PubKey) + require.NoError(t, err) + + testCases := []struct { + name string + votes []*tmtypes.Vote + chainID string + pubkey tmprotocrypto.PublicKey + expPass bool + }{ + { + "evidence has votes with different block height - shouldn't pass", + []*tmtypes.Vote{ + testutil.MakeAndSignVote( + blockID1, + ctx.BlockHeight()+1, + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + testutil.MakeAndSignVote( + blockID2, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + }, + chainID, + valPubkey1, + false, + }, + { + "evidence has votes with different validator address - shouldn't pass", + []*tmtypes.Vote{ + testutil.MakeAndSignVote( + blockID1, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + testutil.MakeAndSignVote( + blockID2, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer2, + chainID, + ), + }, + chainID, + valPubkey1, + false, + }, + { + "evidence has votes with same block IDs - shouldn't pass", + []*tmtypes.Vote{ + testutil.MakeAndSignVote( + blockID1, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + testutil.MakeAndSignVote( + blockID1, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + }, + chainID, + valPubkey1, + false, + }, + { + "given chain ID isn't the same as the one used to sign the votes - shouldn't pass", + []*tmtypes.Vote{ + testutil.MakeAndSignVote( + blockID1, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + testutil.MakeAndSignVote( + blockID2, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + }, + "WrongChainID", + valPubkey1, + false, + }, + { + "voteA is signed using the wrong chain ID - shouldn't pass", + []*tmtypes.Vote{ + testutil.MakeAndSignVote( + blockID1, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + "WrongChainID", + ), + testutil.MakeAndSignVote( + blockID2, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + }, + chainID, + valPubkey1, + false, + }, + { + "voteB is signed using the wrong chain ID - shouldn't pass", + []*tmtypes.Vote{ + testutil.MakeAndSignVote( + blockID1, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + testutil.MakeAndSignVote( + blockID2, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + "WrongChainID", + ), + }, + chainID, + valPubkey1, + false, + }, + { + "invalid public key - shouldn't pass", + []*tmtypes.Vote{ + testutil.MakeAndSignVote( + blockID1, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + testutil.MakeAndSignVote( + blockID2, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + }, + chainID, + tmprotocrypto.PublicKey{}, + false, + }, + { + "wrong public key - shouldn't pass", + []*tmtypes.Vote{ + testutil.MakeAndSignVote( + blockID1, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + testutil.MakeAndSignVote( + blockID2, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + }, + chainID, + valPubkey2, + false, + }, + { + "valid double voting evidence should pass", + []*tmtypes.Vote{ + testutil.MakeAndSignVote( + blockID1, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + testutil.MakeAndSignVote( + blockID2, + ctx.BlockHeight(), + ctx.BlockTime(), + valSet, + signer1, + chainID, + ), + }, + chainID, + valPubkey1, + true, + }, + } + + for _, tc := range testCases { + err = keeper.VerifyDoubleVotingEvidence( + ctx, + tmtypes.DuplicateVoteEvidence{ + VoteA: tc.votes[0], + VoteB: tc.votes[1], + ValidatorPower: val1.VotingPower, + TotalVotingPower: val1.VotingPower, + Timestamp: tc.votes[0].Timestamp, + }, + tc.chainID, + tc.pubkey, + ) + if tc.expPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + } +} diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 0438df36e3..35d9219324 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -5,7 +5,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" ibcclienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types" tmtypes "github.com/tendermint/tendermint/types" @@ -33,36 +32,22 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty return err } + provAddrs := make([]types.ProviderConsAddress, len(byzantineValidators)) + // jail and tombstone the Byzantine validators for _, v := range byzantineValidators { - // convert consumer consensus address - consumerAddr := types.NewConsumerConsAddress(sdk.ConsAddress(v.Address.Bytes())) - providerAddr := k.GetProviderAddrFromConsumerAddr(ctx, misbehaviour.Header1.Header.ChainID, consumerAddr) - val, ok := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) - - if !ok || val.IsUnbonded() { - logger.Error("validator not found or is unbonded", providerAddr.String()) - continue - } - - // jail validator if not already - if !val.IsJailed() { - k.stakingKeeper.Jail(ctx, providerAddr.ToSdkConsAddr()) - } - - // tombstone validator if not already - if !k.slashingKeeper.IsTombstoned(ctx, providerAddr.ToSdkConsAddr()) { - k.slashingKeeper.Tombstone(ctx, providerAddr.ToSdkConsAddr()) - k.Logger(ctx).Info("validator tombstoned", "provider cons addr", providerAddr.String()) - } - - // update jail time to end after double sign jail duration - k.slashingKeeper.JailUntil(ctx, providerAddr.ToSdkConsAddr(), evidencetypes.DoubleSignJailEndTime) + providerAddr := k.GetProviderAddrFromConsumerAddr( + ctx, + misbehaviour.Header1.Header.ChainID, + types.NewConsumerConsAddress(sdk.ConsAddress(v.Address.Bytes())), + ) + k.JailValidator(ctx, providerAddr) + provAddrs = append(provAddrs, providerAddr) } logger.Info( "confirmed equivocation light client attack", - "byzantine validators", byzantineValidators, + "byzantine validators", provAddrs, ) return nil diff --git a/x/ccv/provider/keeper/msg_server.go b/x/ccv/provider/keeper/msg_server.go index 80f325cc1b..b6b96bb010 100644 --- a/x/ccv/provider/keeper/msg_server.go +++ b/x/ccv/provider/keeper/msg_server.go @@ -10,6 +10,7 @@ import ( "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" ccvtypes "github.com/cosmos/interchain-security/v2/x/ccv/types" tmprotocrypto "github.com/tendermint/tendermint/proto/tendermint/crypto" + tmtypes "github.com/tendermint/tendermint/types" ) type msgServer struct { @@ -146,3 +147,27 @@ func (k msgServer) SubmitConsumerMisbehaviour(goCtx context.Context, msg *types. return &types.MsgSubmitConsumerMisbehaviourResponse{}, nil } + +func (k msgServer) SubmitConsumerDoubleVoting(goCtx context.Context, msg *types.MsgSubmitConsumerDoubleVoting) (*types.MsgSubmitConsumerDoubleVotingResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + evidence, err := tmtypes.DuplicateVoteEvidenceFromProto(msg.DuplicateVoteEvidence) + if err != nil { + return nil, err + } + + if err := k.Keeper.HandleConsumerDoubleVoting(ctx, evidence, msg.InfractionBlockHeader.Header.ChainID); err != nil { + return &types.MsgSubmitConsumerDoubleVotingResponse{}, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + ccvtypes.EventTypeSubmitConsumerMisbehaviour, + sdk.NewAttribute(ccvtypes.AttributeConsumerDoubleVoting, msg.DuplicateVoteEvidence.String()), + sdk.NewAttribute(ccvtypes.AttributeChainID, msg.InfractionBlockHeader.Header.ChainID), + sdk.NewAttribute(ccvtypes.AttributeSubmitterAddress, msg.Submitter), + ), + }) + + return &types.MsgSubmitConsumerDoubleVotingResponse{}, nil +} diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go new file mode 100644 index 0000000000..f4648cc641 --- /dev/null +++ b/x/ccv/provider/keeper/punish_validator.go @@ -0,0 +1,38 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" +) + +// JailValidator jails the validator with the given provider consensus address +// Note that the tombstoning is temporarily removed until we slash validator +// for double signing on a consumer chain, see comment +// https://github.com/cosmos/interchain-security/pull/1232#issuecomment-1693127641. +func (k Keeper) JailValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) { + logger := k.Logger(ctx) + + // get validator + val, ok := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) + if !ok || val.IsUnbonded() { + logger.Error("validator not found or is unbonded", providerAddr.String()) + return + } + + // check that the validator isn't tombstoned + if k.slashingKeeper.IsTombstoned(ctx, providerAddr.ToSdkConsAddr()) { + logger.Info("validator is already tombstoned", "provider cons addr", providerAddr.String()) + return + } + + // jail validator if not already + if !val.IsJailed() { + k.stakingKeeper.Jail(ctx, providerAddr.ToSdkConsAddr()) + } + + // update jail time to end after double sign jail duration + k.slashingKeeper.JailUntil(ctx, providerAddr.ToSdkConsAddr(), evidencetypes.DoubleSignJailEndTime) + + // TODO: add tombstoning back once we integrate the slashing +} diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go new file mode 100644 index 0000000000..50da9ae4bb --- /dev/null +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -0,0 +1,132 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + cryptotestutil "github.com/cosmos/interchain-security/v2/testutil/crypto" + testkeeper "github.com/cosmos/interchain-security/v2/testutil/keeper" + "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" + "github.com/golang/mock/gomock" +) + +// TestJailValidator tests that the jailing of a validator is only executed +// under the conditions that the validator is neither unbonded, already jailed, nor tombstoned. +func TestJailValidator(t *testing.T) { + providerConsAddr := cryptotestutil.NewCryptoIdentityFromIntSeed(7842334).ProviderConsAddress() + testCases := []struct { + name string + provAddr types.ProviderConsAddress + expectedCalls func(sdk.Context, testkeeper.MockedKeepers, types.ProviderConsAddress) []*gomock.Call + }{ + { + "unfound validator", + providerConsAddr, + func(ctx sdk.Context, mocks testkeeper.MockedKeepers, + provAddr types.ProviderConsAddress, + ) []*gomock.Call { + return []*gomock.Call{ + // We only expect a single call to GetValidatorByConsAddr. + // Method will return once validator is not found. + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr( + ctx, providerConsAddr.ToSdkConsAddr()).Return( + stakingtypes.Validator{}, false, // false = Not found. + ).Times(1), + } + }, + }, + { + "unbonded validator", + providerConsAddr, + func(ctx sdk.Context, mocks testkeeper.MockedKeepers, + provAddr types.ProviderConsAddress, + ) []*gomock.Call { + return []*gomock.Call{ + // We only expect a single call to GetValidatorByConsAddr. + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr( + ctx, providerConsAddr.ToSdkConsAddr()).Return( + stakingtypes.Validator{Status: stakingtypes.Unbonded}, true, + ).Times(1), + } + }, + }, + { + "tombstoned validator", + providerConsAddr, + func(ctx sdk.Context, mocks testkeeper.MockedKeepers, + provAddr types.ProviderConsAddress, + ) []*gomock.Call { + return []*gomock.Call{ + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr( + ctx, providerConsAddr.ToSdkConsAddr()).Return( + stakingtypes.Validator{}, true, + ).Times(1), + mocks.MockSlashingKeeper.EXPECT().IsTombstoned( + ctx, providerConsAddr.ToSdkConsAddr()).Return( + true, + ).Times(1), + } + }, + }, + { + "jailed validator", + providerConsAddr, + func(ctx sdk.Context, mocks testkeeper.MockedKeepers, + provAddr types.ProviderConsAddress, + ) []*gomock.Call { + return []*gomock.Call{ + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr( + ctx, providerConsAddr.ToSdkConsAddr()).Return( + stakingtypes.Validator{Jailed: true}, true, + ).Times(1), + mocks.MockSlashingKeeper.EXPECT().IsTombstoned( + ctx, providerConsAddr.ToSdkConsAddr()).Return( + false, + ).Times(1), + mocks.MockSlashingKeeper.EXPECT().JailUntil( + ctx, providerConsAddr.ToSdkConsAddr(), evidencetypes.DoubleSignJailEndTime). + Times(1), + } + }, + }, + { + "bonded validator", + providerConsAddr, + func(ctx sdk.Context, mocks testkeeper.MockedKeepers, + provAddr types.ProviderConsAddress, + ) []*gomock.Call { + return []*gomock.Call{ + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr( + ctx, providerConsAddr.ToSdkConsAddr()).Return( + stakingtypes.Validator{Status: stakingtypes.Bonded}, true, + ).Times(1), + mocks.MockSlashingKeeper.EXPECT().IsTombstoned( + ctx, providerConsAddr.ToSdkConsAddr()).Return( + false, + ).Times(1), + mocks.MockStakingKeeper.EXPECT().Jail( + ctx, providerConsAddr.ToSdkConsAddr()). + Times(1), + mocks.MockSlashingKeeper.EXPECT().JailUntil( + ctx, providerConsAddr.ToSdkConsAddr(), evidencetypes.DoubleSignJailEndTime). + Times(1), + } + }, + }, + } + + for _, tc := range testCases { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx( + t, testkeeper.NewInMemKeeperParams(t)) + + // Setup expected mock calls + gomock.InOrder(tc.expectedCalls(ctx, mocks, tc.provAddr)...) + + // Execute method and assert expected mock calls + providerKeeper.JailValidator(ctx, tc.provAddr) + + ctrl.Finish() + } +} diff --git a/x/ccv/provider/types/msg.go b/x/ccv/provider/types/msg.go index 5c217f53b8..e5dbb16697 100644 --- a/x/ccv/provider/types/msg.go +++ b/x/ccv/provider/types/msg.go @@ -2,11 +2,13 @@ package types import ( "encoding/json" + "fmt" "strings" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types" + tmtypes "github.com/tendermint/tendermint/proto/tendermint/types" ) // provider message types @@ -14,6 +16,7 @@ const ( TypeMsgAssignConsumerKey = "assign_consumer_key" TypeMsgRegisterConsumerRewardDenom = "register_consumer_reward_denom" TypeMsgSubmitConsumerMisbehaviour = "submit_consumer_misbehaviour" + TypeMsgSubmitConsumerDoubleVoting = "submit_consumer_double_vote" ) var ( @@ -185,3 +188,47 @@ func (msg MsgSubmitConsumerMisbehaviour) GetSigners() []sdk.AccAddress { } return []sdk.AccAddress{addr} } + +func NewMsgSubmitConsumerDoubleVoting(submitter sdk.AccAddress, ev *tmtypes.DuplicateVoteEvidence, header *ibctmtypes.Header) (*MsgSubmitConsumerDoubleVoting, error) { + return &MsgSubmitConsumerDoubleVoting{Submitter: submitter.String(), DuplicateVoteEvidence: ev, InfractionBlockHeader: header}, nil +} + +// Route implements the sdk.Msg interface. +func (msg MsgSubmitConsumerDoubleVoting) Route() string { return RouterKey } + +// Type implements the sdk.Msg interface. +func (msg MsgSubmitConsumerDoubleVoting) Type() string { + return TypeMsgSubmitConsumerDoubleVoting +} + +// Type implements the sdk.Msg interface. +func (msg MsgSubmitConsumerDoubleVoting) ValidateBasic() error { + if msg.Submitter == "" { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Submitter) + } + if msg.DuplicateVoteEvidence == nil { + return fmt.Errorf("double voting evidence cannot be nil") + } + + if msg.InfractionBlockHeader.Header == nil { + return fmt.Errorf("infraction header cannot be nil") + } + + return nil +} + +// Type implements the sdk.Msg interface. +func (msg MsgSubmitConsumerDoubleVoting) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(&msg) + return sdk.MustSortJSON(bz) +} + +// Type implements the sdk.Msg interface. +func (msg MsgSubmitConsumerDoubleVoting) GetSigners() []sdk.AccAddress { + addr, err := sdk.AccAddressFromBech32(msg.Submitter) + if err != nil { + // same behavior as in cosmos-sdk + panic(err) + } + return []sdk.AccAddress{addr} +} diff --git a/x/ccv/provider/types/tx.pb.go b/x/ccv/provider/types/tx.pb.go index f27ab52533..9182d546b5 100644 --- a/x/ccv/provider/types/tx.pb.go +++ b/x/ccv/provider/types/tx.pb.go @@ -12,6 +12,7 @@ import ( grpc1 "github.com/gogo/protobuf/grpc" proto "github.com/gogo/protobuf/proto" _ "github.com/regen-network/cosmos-proto" + types1 "github.com/tendermint/tendermint/proto/tendermint/types" _ "google.golang.org/genproto/googleapis/api/annotations" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" @@ -271,6 +272,86 @@ func (m *MsgSubmitConsumerMisbehaviourResponse) XXX_DiscardUnknown() { var xxx_messageInfo_MsgSubmitConsumerMisbehaviourResponse proto.InternalMessageInfo +// MsgSubmitConsumerDoubleVoting defines a message that reports an equivocation +// observed on a consumer chain +type MsgSubmitConsumerDoubleVoting struct { + Submitter string `protobuf:"bytes,1,opt,name=submitter,proto3" json:"submitter,omitempty"` + // The equivocation of the consumer chain wrapping + // an evidence of a validator that signed two conflicting votes + DuplicateVoteEvidence *types1.DuplicateVoteEvidence `protobuf:"bytes,2,opt,name=duplicate_vote_evidence,json=duplicateVoteEvidence,proto3" json:"duplicate_vote_evidence,omitempty"` + // The light client header of the infraction block + InfractionBlockHeader *types.Header `protobuf:"bytes,3,opt,name=infraction_block_header,json=infractionBlockHeader,proto3" json:"infraction_block_header,omitempty"` +} + +func (m *MsgSubmitConsumerDoubleVoting) Reset() { *m = MsgSubmitConsumerDoubleVoting{} } +func (m *MsgSubmitConsumerDoubleVoting) String() string { return proto.CompactTextString(m) } +func (*MsgSubmitConsumerDoubleVoting) ProtoMessage() {} +func (*MsgSubmitConsumerDoubleVoting) Descriptor() ([]byte, []int) { + return fileDescriptor_43221a4391e9fbf4, []int{6} +} +func (m *MsgSubmitConsumerDoubleVoting) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgSubmitConsumerDoubleVoting) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgSubmitConsumerDoubleVoting.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgSubmitConsumerDoubleVoting) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgSubmitConsumerDoubleVoting.Merge(m, src) +} +func (m *MsgSubmitConsumerDoubleVoting) XXX_Size() int { + return m.Size() +} +func (m *MsgSubmitConsumerDoubleVoting) XXX_DiscardUnknown() { + xxx_messageInfo_MsgSubmitConsumerDoubleVoting.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgSubmitConsumerDoubleVoting proto.InternalMessageInfo + +type MsgSubmitConsumerDoubleVotingResponse struct { +} + +func (m *MsgSubmitConsumerDoubleVotingResponse) Reset() { *m = MsgSubmitConsumerDoubleVotingResponse{} } +func (m *MsgSubmitConsumerDoubleVotingResponse) String() string { return proto.CompactTextString(m) } +func (*MsgSubmitConsumerDoubleVotingResponse) ProtoMessage() {} +func (*MsgSubmitConsumerDoubleVotingResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_43221a4391e9fbf4, []int{7} +} +func (m *MsgSubmitConsumerDoubleVotingResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgSubmitConsumerDoubleVotingResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgSubmitConsumerDoubleVotingResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgSubmitConsumerDoubleVotingResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgSubmitConsumerDoubleVotingResponse.Merge(m, src) +} +func (m *MsgSubmitConsumerDoubleVotingResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgSubmitConsumerDoubleVotingResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgSubmitConsumerDoubleVotingResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgSubmitConsumerDoubleVotingResponse proto.InternalMessageInfo + func init() { proto.RegisterType((*MsgAssignConsumerKey)(nil), "interchain_security.ccv.provider.v1.MsgAssignConsumerKey") proto.RegisterType((*MsgAssignConsumerKeyResponse)(nil), "interchain_security.ccv.provider.v1.MsgAssignConsumerKeyResponse") @@ -278,6 +359,8 @@ func init() { proto.RegisterType((*MsgRegisterConsumerRewardDenomResponse)(nil), "interchain_security.ccv.provider.v1.MsgRegisterConsumerRewardDenomResponse") proto.RegisterType((*MsgSubmitConsumerMisbehaviour)(nil), "interchain_security.ccv.provider.v1.MsgSubmitConsumerMisbehaviour") proto.RegisterType((*MsgSubmitConsumerMisbehaviourResponse)(nil), "interchain_security.ccv.provider.v1.MsgSubmitConsumerMisbehaviourResponse") + proto.RegisterType((*MsgSubmitConsumerDoubleVoting)(nil), "interchain_security.ccv.provider.v1.MsgSubmitConsumerDoubleVoting") + proto.RegisterType((*MsgSubmitConsumerDoubleVotingResponse)(nil), "interchain_security.ccv.provider.v1.MsgSubmitConsumerDoubleVotingResponse") } func init() { @@ -285,43 +368,51 @@ func init() { } var fileDescriptor_43221a4391e9fbf4 = []byte{ - // 568 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x54, 0x3f, 0x6f, 0x13, 0x4f, - 0x10, 0xf5, 0xfd, 0xa2, 0x1f, 0x24, 0x9b, 0x80, 0xc4, 0xc9, 0x85, 0x73, 0x98, 0x33, 0x18, 0x01, - 0x29, 0xc2, 0xae, 0x6c, 0x0a, 0x44, 0x24, 0x0a, 0x3b, 0x34, 0x10, 0x59, 0x42, 0x47, 0x81, 0x44, - 0x81, 0x75, 0xb7, 0xbb, 0xac, 0x57, 0xf8, 0x76, 0x4f, 0xbb, 0x7b, 0x47, 0xee, 0x1b, 0x50, 0x42, - 0x85, 0xe8, 0xf2, 0x01, 0x90, 0xf8, 0x1a, 0x94, 0x29, 0xa9, 0x10, 0xb2, 0x1b, 0x6a, 0x4a, 0x2a, - 0xe4, 0xfb, 0x63, 0x5f, 0x84, 0xb1, 0x2c, 0xa0, 0xdb, 0x99, 0x79, 0xfb, 0xde, 0x1b, 0xcd, 0x68, - 0xc0, 0x3e, 0x17, 0x86, 0x2a, 0x3c, 0xf2, 0xb9, 0x18, 0x6a, 0x8a, 0x63, 0xc5, 0x4d, 0x8a, 0x30, - 0x4e, 0x50, 0xa4, 0x64, 0xc2, 0x09, 0x55, 0x28, 0xe9, 0x20, 0x73, 0x0c, 0x23, 0x25, 0x8d, 0xb4, - 0xaf, 0x2f, 0x41, 0x43, 0x8c, 0x13, 0x58, 0xa2, 0x61, 0xd2, 0x71, 0x9a, 0x4c, 0x4a, 0x36, 0xa6, - 0xc8, 0x8f, 0x38, 0xf2, 0x85, 0x90, 0xc6, 0x37, 0x5c, 0x0a, 0x9d, 0x53, 0x38, 0x75, 0x26, 0x99, - 0xcc, 0x9e, 0x68, 0xf6, 0x2a, 0xb2, 0xbb, 0x58, 0xea, 0x50, 0xea, 0x61, 0x5e, 0xc8, 0x83, 0xb2, - 0x54, 0xd0, 0x65, 0x51, 0x10, 0xbf, 0x40, 0xbe, 0x48, 0x8b, 0x12, 0xe2, 0x01, 0x46, 0x63, 0xce, - 0x46, 0x06, 0x8f, 0x39, 0x15, 0x46, 0x23, 0x43, 0x05, 0xa1, 0x2a, 0xe4, 0xc2, 0x64, 0xbe, 0xe7, - 0x51, 0xfe, 0xa1, 0xfd, 0xce, 0x02, 0xf5, 0x81, 0x66, 0x3d, 0xad, 0x39, 0x13, 0x87, 0x52, 0xe8, - 0x38, 0xa4, 0xea, 0x88, 0xa6, 0xf6, 0x2e, 0xd8, 0xcc, 0xbb, 0xe2, 0xa4, 0x61, 0x5d, 0xb5, 0xf6, - 0xb6, 0xbc, 0xf3, 0x59, 0xfc, 0x90, 0xd8, 0x77, 0xc1, 0x85, 0xb2, 0xbb, 0xa1, 0x4f, 0x88, 0x6a, - 0xfc, 0x37, 0xab, 0xf7, 0xed, 0xef, 0x5f, 0x5a, 0x17, 0x53, 0x3f, 0x1c, 0x1f, 0xb4, 0x67, 0x59, - 0xaa, 0x75, 0xdb, 0xdb, 0x29, 0x81, 0x3d, 0x42, 0x94, 0x7d, 0x0d, 0xec, 0xe0, 0x42, 0x62, 0xf8, - 0x92, 0xa6, 0x8d, 0x8d, 0x8c, 0x77, 0x1b, 0x2f, 0x64, 0x0f, 0x36, 0x5f, 0x9f, 0xb4, 0x6a, 0xdf, - 0x4e, 0x5a, 0xb5, 0xb6, 0x0b, 0x9a, 0xcb, 0x8c, 0x79, 0x54, 0x47, 0x52, 0x68, 0xda, 0x7e, 0x0e, - 0xdc, 0x81, 0x66, 0x1e, 0x65, 0x5c, 0x1b, 0xaa, 0x4a, 0x84, 0x47, 0x5f, 0xf9, 0x8a, 0x3c, 0xa0, - 0x42, 0x86, 0x76, 0x1d, 0xfc, 0x4f, 0x66, 0x8f, 0xc2, 0x7f, 0x1e, 0xd8, 0x4d, 0xb0, 0x45, 0x68, - 0x24, 0x35, 0x37, 0xb2, 0x70, 0xee, 0x2d, 0x12, 0x15, 0xfd, 0x3d, 0x70, 0x73, 0x35, 0xff, 0xdc, - 0xc9, 0x7b, 0x0b, 0x5c, 0x19, 0x68, 0xf6, 0x24, 0x0e, 0x42, 0x6e, 0x4a, 0xe0, 0x80, 0xeb, 0x80, - 0x8e, 0xfc, 0x84, 0xcb, 0x58, 0xcd, 0x34, 0x75, 0x56, 0x35, 0x54, 0x15, 0x6e, 0x16, 0x09, 0xfb, - 0x31, 0xd8, 0x09, 0x2b, 0xe8, 0xcc, 0xd4, 0x76, 0x77, 0x1f, 0xf2, 0x00, 0xc3, 0xea, 0x2c, 0x61, - 0x65, 0x7a, 0x49, 0x07, 0x56, 0x15, 0xbc, 0x33, 0x0c, 0x95, 0x2e, 0x6e, 0x81, 0x1b, 0x2b, 0xad, - 0x95, 0x4d, 0x74, 0x7f, 0x6c, 0x80, 0x8d, 0x81, 0x66, 0xf6, 0x5b, 0x0b, 0x5c, 0xfa, 0x75, 0x1b, - 0xee, 0xc1, 0x35, 0xf6, 0x1c, 0x2e, 0x9b, 0x97, 0xd3, 0xfb, 0xe3, 0xaf, 0xa5, 0x37, 0xfb, 0xa3, - 0x05, 0x2e, 0xaf, 0x1a, 0xf4, 0xe1, 0xba, 0x12, 0x2b, 0x48, 0x9c, 0xa3, 0x7f, 0x40, 0x32, 0x77, - 0xfc, 0xc1, 0x02, 0xce, 0x8a, 0x7d, 0xe8, 0xaf, 0xab, 0xf5, 0x7b, 0x0e, 0xe7, 0xd1, 0xdf, 0x73, - 0x94, 0x76, 0xfb, 0x4f, 0x3f, 0x4d, 0x5c, 0xeb, 0x74, 0xe2, 0x5a, 0x5f, 0x27, 0xae, 0xf5, 0x66, - 0xea, 0xd6, 0x4e, 0xa7, 0x6e, 0xed, 0xf3, 0xd4, 0xad, 0x3d, 0xbb, 0xcf, 0xb8, 0x19, 0xc5, 0x01, - 0xc4, 0x32, 0x2c, 0x8e, 0x10, 0x5a, 0xc8, 0xde, 0x9e, 0xdf, 0xc7, 0xa4, 0x8b, 0x8e, 0xcf, 0x1e, - 0x49, 0x93, 0x46, 0x54, 0x07, 0xe7, 0xb2, 0x2b, 0x73, 0xe7, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, - 0xe3, 0x4f, 0x57, 0x26, 0x55, 0x05, 0x00, 0x00, + // 694 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x55, 0xcf, 0x4e, 0x14, 0x4f, + 0x10, 0xde, 0x81, 0xf0, 0xfb, 0x41, 0x83, 0x26, 0x4e, 0x20, 0xc0, 0x8a, 0xb3, 0xba, 0x46, 0xe0, + 0x80, 0xd3, 0x61, 0x3d, 0x18, 0x49, 0x3c, 0xb0, 0x60, 0xa2, 0x92, 0x4d, 0xcc, 0x98, 0x60, 0xe2, + 0x81, 0xc9, 0x4c, 0x77, 0x31, 0xdb, 0x61, 0xa7, 0x7b, 0xd3, 0xdd, 0x33, 0xb2, 0x6f, 0xc0, 0x51, + 0x4f, 0xc6, 0x1b, 0x57, 0x13, 0x13, 0x5f, 0xc3, 0x23, 0x47, 0x4f, 0xc6, 0xc0, 0xc5, 0xb3, 0x4f, + 0x60, 0xe6, 0xdf, 0xee, 0x10, 0xd7, 0x85, 0xa0, 0xb7, 0xae, 0xaa, 0xaf, 0xbf, 0xfa, 0xaa, 0xa7, + 0x6a, 0x0a, 0xad, 0x31, 0xae, 0x41, 0x92, 0xb6, 0xc7, 0xb8, 0xab, 0x80, 0x44, 0x92, 0xe9, 0x1e, + 0x26, 0x24, 0xc6, 0x5d, 0x29, 0x62, 0x46, 0x41, 0xe2, 0x78, 0x1d, 0xeb, 0x43, 0xbb, 0x2b, 0x85, + 0x16, 0xe6, 0xdd, 0x21, 0x68, 0x9b, 0x90, 0xd8, 0x2e, 0xd0, 0x76, 0xbc, 0x5e, 0x5d, 0x0a, 0x84, + 0x08, 0x3a, 0x80, 0xbd, 0x2e, 0xc3, 0x1e, 0xe7, 0x42, 0x7b, 0x9a, 0x09, 0xae, 0x32, 0x8a, 0xea, + 0x6c, 0x20, 0x02, 0x91, 0x1e, 0x71, 0x72, 0xca, 0xbd, 0x8b, 0x44, 0xa8, 0x50, 0x28, 0x37, 0x0b, + 0x64, 0x46, 0x11, 0xca, 0xe9, 0x52, 0xcb, 0x8f, 0xf6, 0xb1, 0xc7, 0x7b, 0x79, 0x08, 0x33, 0x9f, + 0xe0, 0x0e, 0x0b, 0xda, 0x9a, 0x74, 0x18, 0x70, 0xad, 0xb0, 0x06, 0x4e, 0x41, 0x86, 0x8c, 0xeb, + 0x54, 0x77, 0xdf, 0xca, 0x2f, 0xd4, 0x4a, 0x71, 0xdd, 0xeb, 0x82, 0xc2, 0x90, 0xc8, 0xe6, 0x04, + 0x32, 0x40, 0xfd, 0xbd, 0x81, 0x66, 0x5b, 0x2a, 0xd8, 0x54, 0x8a, 0x05, 0x7c, 0x4b, 0x70, 0x15, + 0x85, 0x20, 0x77, 0xa0, 0x67, 0x2e, 0xa2, 0xc9, 0xac, 0x6c, 0x46, 0x17, 0x8c, 0xdb, 0xc6, 0xea, + 0x94, 0xf3, 0x7f, 0x6a, 0x3f, 0xa3, 0xe6, 0x43, 0x74, 0xad, 0x28, 0xdf, 0xf5, 0x28, 0x95, 0x0b, + 0x63, 0x49, 0xbc, 0x69, 0xfe, 0xfc, 0x56, 0xbb, 0xde, 0xf3, 0xc2, 0xce, 0x46, 0x3d, 0xf1, 0x82, + 0x52, 0x75, 0x67, 0xa6, 0x00, 0x6e, 0x52, 0x2a, 0xcd, 0x3b, 0x68, 0x86, 0xe4, 0x29, 0xdc, 0x03, + 0xe8, 0x2d, 0x8c, 0xa7, 0xbc, 0xd3, 0x64, 0x90, 0x76, 0x63, 0xf2, 0xe8, 0xb8, 0x56, 0xf9, 0x71, + 0x5c, 0xab, 0xd4, 0x2d, 0xb4, 0x34, 0x4c, 0x98, 0x03, 0xaa, 0x2b, 0xb8, 0x82, 0xfa, 0x1e, 0xb2, + 0x5a, 0x2a, 0x70, 0x20, 0x60, 0x4a, 0x83, 0x2c, 0x10, 0x0e, 0xbc, 0xf1, 0x24, 0xdd, 0x06, 0x2e, + 0x42, 0x73, 0x16, 0x4d, 0xd0, 0xe4, 0x90, 0xeb, 0xcf, 0x0c, 0x73, 0x09, 0x4d, 0x51, 0xe8, 0x0a, + 0xc5, 0xb4, 0xc8, 0x95, 0x3b, 0x03, 0x47, 0x29, 0xff, 0x2a, 0x5a, 0x1e, 0xcd, 0xdf, 0x57, 0xf2, + 0xc1, 0x40, 0xb7, 0x5a, 0x2a, 0x78, 0x19, 0xf9, 0x21, 0xd3, 0x05, 0xb0, 0xc5, 0x94, 0x0f, 0x6d, + 0x2f, 0x66, 0x22, 0x92, 0x49, 0x4e, 0x95, 0x46, 0x35, 0xc8, 0x5c, 0xcd, 0xc0, 0x61, 0xbe, 0x40, + 0x33, 0x61, 0x09, 0x9d, 0x8a, 0x9a, 0x6e, 0xac, 0xd9, 0xcc, 0x27, 0x76, 0xf9, 0x63, 0xdb, 0xa5, + 0xcf, 0x1b, 0xaf, 0xdb, 0xe5, 0x0c, 0xce, 0x39, 0x86, 0x52, 0x15, 0x2b, 0xe8, 0xde, 0x48, 0x69, + 0xfd, 0x22, 0x8e, 0xc6, 0x86, 0x14, 0xb1, 0x2d, 0x22, 0xbf, 0x03, 0xbb, 0x42, 0x33, 0x1e, 0x5c, + 0x50, 0x84, 0x8b, 0xe6, 0x69, 0xd4, 0xed, 0x30, 0xe2, 0x69, 0x70, 0x63, 0xa1, 0xc1, 0x2d, 0x3a, + 0x2d, 0xaf, 0x67, 0xa5, 0x2c, 0x3f, 0xed, 0x45, 0x7b, 0xbb, 0xb8, 0xb0, 0x2b, 0x34, 0x3c, 0xc9, + 0xe1, 0xce, 0x1c, 0x1d, 0xe6, 0x36, 0xf7, 0xd0, 0x3c, 0xe3, 0xfb, 0xd2, 0x23, 0xc9, 0x70, 0xb9, + 0x7e, 0x47, 0x90, 0x03, 0xb7, 0x0d, 0x1e, 0x05, 0x99, 0xf6, 0xd1, 0x74, 0x63, 0xf9, 0xa2, 0x07, + 0x7b, 0x9a, 0xa2, 0x9d, 0xb9, 0x01, 0x4d, 0x33, 0x61, 0xc9, 0xdc, 0x17, 0xbc, 0x59, 0xf9, 0x25, + 0x8a, 0x37, 0x6b, 0x7c, 0x9c, 0x40, 0xe3, 0x2d, 0x15, 0x98, 0xef, 0x0c, 0x74, 0xe3, 0xf7, 0x09, + 0x7a, 0x64, 0x5f, 0xe2, 0xe7, 0x61, 0x0f, 0xeb, 0xf1, 0xea, 0xe6, 0x95, 0xaf, 0x16, 0xda, 0xcc, + 0xcf, 0x06, 0xba, 0x39, 0x6a, 0x38, 0xb6, 0x2e, 0x9b, 0x62, 0x04, 0x49, 0x75, 0xe7, 0x1f, 0x90, + 0xf4, 0x15, 0x7f, 0x32, 0x50, 0x75, 0xc4, 0x0c, 0x35, 0x2f, 0x9b, 0xeb, 0xcf, 0x1c, 0xd5, 0xe7, + 0x7f, 0xcf, 0x31, 0x42, 0xee, 0xb9, 0x69, 0xb9, 0xa2, 0xdc, 0x32, 0xc7, 0x55, 0xe5, 0x0e, 0xeb, + 0xd5, 0xe6, 0xab, 0x2f, 0xa7, 0x96, 0x71, 0x72, 0x6a, 0x19, 0xdf, 0x4f, 0x2d, 0xe3, 0xed, 0x99, + 0x55, 0x39, 0x39, 0xb3, 0x2a, 0x5f, 0xcf, 0xac, 0xca, 0xeb, 0xc7, 0x01, 0xd3, 0xed, 0xc8, 0xb7, + 0x89, 0x08, 0xf3, 0x45, 0x84, 0x07, 0x69, 0xef, 0xf7, 0x77, 0x64, 0xdc, 0xc0, 0x87, 0xe7, 0x17, + 0x65, 0x3a, 0xc4, 0xfe, 0x7f, 0xe9, 0x22, 0x79, 0xf0, 0x2b, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x25, + 0x43, 0x02, 0x59, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -339,6 +430,7 @@ type MsgClient interface { AssignConsumerKey(ctx context.Context, in *MsgAssignConsumerKey, opts ...grpc.CallOption) (*MsgAssignConsumerKeyResponse, error) RegisterConsumerRewardDenom(ctx context.Context, in *MsgRegisterConsumerRewardDenom, opts ...grpc.CallOption) (*MsgRegisterConsumerRewardDenomResponse, error) SubmitConsumerMisbehaviour(ctx context.Context, in *MsgSubmitConsumerMisbehaviour, opts ...grpc.CallOption) (*MsgSubmitConsumerMisbehaviourResponse, error) + SubmitConsumerDoubleVoting(ctx context.Context, in *MsgSubmitConsumerDoubleVoting, opts ...grpc.CallOption) (*MsgSubmitConsumerDoubleVotingResponse, error) } type msgClient struct { @@ -376,11 +468,21 @@ func (c *msgClient) SubmitConsumerMisbehaviour(ctx context.Context, in *MsgSubmi return out, nil } +func (c *msgClient) SubmitConsumerDoubleVoting(ctx context.Context, in *MsgSubmitConsumerDoubleVoting, opts ...grpc.CallOption) (*MsgSubmitConsumerDoubleVotingResponse, error) { + out := new(MsgSubmitConsumerDoubleVotingResponse) + err := c.cc.Invoke(ctx, "/interchain_security.ccv.provider.v1.Msg/SubmitConsumerDoubleVoting", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // MsgServer is the server API for Msg service. type MsgServer interface { AssignConsumerKey(context.Context, *MsgAssignConsumerKey) (*MsgAssignConsumerKeyResponse, error) RegisterConsumerRewardDenom(context.Context, *MsgRegisterConsumerRewardDenom) (*MsgRegisterConsumerRewardDenomResponse, error) SubmitConsumerMisbehaviour(context.Context, *MsgSubmitConsumerMisbehaviour) (*MsgSubmitConsumerMisbehaviourResponse, error) + SubmitConsumerDoubleVoting(context.Context, *MsgSubmitConsumerDoubleVoting) (*MsgSubmitConsumerDoubleVotingResponse, error) } // UnimplementedMsgServer can be embedded to have forward compatible implementations. @@ -396,6 +498,9 @@ func (*UnimplementedMsgServer) RegisterConsumerRewardDenom(ctx context.Context, func (*UnimplementedMsgServer) SubmitConsumerMisbehaviour(ctx context.Context, req *MsgSubmitConsumerMisbehaviour) (*MsgSubmitConsumerMisbehaviourResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SubmitConsumerMisbehaviour not implemented") } +func (*UnimplementedMsgServer) SubmitConsumerDoubleVoting(ctx context.Context, req *MsgSubmitConsumerDoubleVoting) (*MsgSubmitConsumerDoubleVotingResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SubmitConsumerDoubleVoting not implemented") +} func RegisterMsgServer(s grpc1.Server, srv MsgServer) { s.RegisterService(&_Msg_serviceDesc, srv) @@ -455,6 +560,24 @@ func _Msg_SubmitConsumerMisbehaviour_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _Msg_SubmitConsumerDoubleVoting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgSubmitConsumerDoubleVoting) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).SubmitConsumerDoubleVoting(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/interchain_security.ccv.provider.v1.Msg/SubmitConsumerDoubleVoting", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).SubmitConsumerDoubleVoting(ctx, req.(*MsgSubmitConsumerDoubleVoting)) + } + return interceptor(ctx, in, info, handler) +} + var _Msg_serviceDesc = grpc.ServiceDesc{ ServiceName: "interchain_security.ccv.provider.v1.Msg", HandlerType: (*MsgServer)(nil), @@ -471,6 +594,10 @@ var _Msg_serviceDesc = grpc.ServiceDesc{ MethodName: "SubmitConsumerMisbehaviour", Handler: _Msg_SubmitConsumerMisbehaviour_Handler, }, + { + MethodName: "SubmitConsumerDoubleVoting", + Handler: _Msg_SubmitConsumerDoubleVoting_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "interchain_security/ccv/provider/v1/tx.proto", @@ -668,6 +795,83 @@ func (m *MsgSubmitConsumerMisbehaviourResponse) MarshalToSizedBuffer(dAtA []byte return len(dAtA) - i, nil } +func (m *MsgSubmitConsumerDoubleVoting) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgSubmitConsumerDoubleVoting) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgSubmitConsumerDoubleVoting) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.InfractionBlockHeader != nil { + { + size, err := m.InfractionBlockHeader.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + if m.DuplicateVoteEvidence != nil { + { + size, err := m.DuplicateVoteEvidence.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if len(m.Submitter) > 0 { + i -= len(m.Submitter) + copy(dAtA[i:], m.Submitter) + i = encodeVarintTx(dAtA, i, uint64(len(m.Submitter))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MsgSubmitConsumerDoubleVotingResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgSubmitConsumerDoubleVotingResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgSubmitConsumerDoubleVotingResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + func encodeVarintTx(dAtA []byte, offset int, v uint64) int { offset -= sovTx(v) base := offset @@ -761,6 +965,36 @@ func (m *MsgSubmitConsumerMisbehaviourResponse) Size() (n int) { return n } +func (m *MsgSubmitConsumerDoubleVoting) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Submitter) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if m.DuplicateVoteEvidence != nil { + l = m.DuplicateVoteEvidence.Size() + n += 1 + l + sovTx(uint64(l)) + } + if m.InfractionBlockHeader != nil { + l = m.InfractionBlockHeader.Size() + n += 1 + l + sovTx(uint64(l)) + } + return n +} + +func (m *MsgSubmitConsumerDoubleVotingResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + func sovTx(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -1295,6 +1529,210 @@ func (m *MsgSubmitConsumerMisbehaviourResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *MsgSubmitConsumerDoubleVoting) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgSubmitConsumerDoubleVoting: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgSubmitConsumerDoubleVoting: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Submitter", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Submitter = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DuplicateVoteEvidence", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.DuplicateVoteEvidence == nil { + m.DuplicateVoteEvidence = &types1.DuplicateVoteEvidence{} + } + if err := m.DuplicateVoteEvidence.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field InfractionBlockHeader", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.InfractionBlockHeader == nil { + m.InfractionBlockHeader = &types.Header{} + } + if err := m.InfractionBlockHeader.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgSubmitConsumerDoubleVotingResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgSubmitConsumerDoubleVotingResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgSubmitConsumerDoubleVotingResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipTx(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/ccv/types/errors.go b/x/ccv/types/errors.go index 79c0e1e31c..e0cb663219 100644 --- a/x/ccv/types/errors.go +++ b/x/ccv/types/errors.go @@ -25,4 +25,5 @@ var ( ErrClientNotFound = errorsmod.Register(ModuleName, 18, "client not found") ErrDuplicateConsumerChain = errorsmod.Register(ModuleName, 19, "consumer chain already exists") ErrConsumerChainNotFound = errorsmod.Register(ModuleName, 20, "consumer chain not found") + ErrInvalidEvidence = errorsmod.Register(ModuleName, 21, "invalid consumer double voting evidence") ) diff --git a/x/ccv/types/events.go b/x/ccv/types/events.go index df91333fa0..28796144fe 100644 --- a/x/ccv/types/events.go +++ b/x/ccv/types/events.go @@ -38,6 +38,7 @@ const ( AttributeMisbehaviourClientId = "misbehaviour_client_id" AttributeMisbehaviourHeight1 = "misbehaviour_height_1" AttributeMisbehaviourHeight2 = "misbehaviour_height_2" + AttributeConsumerDoubleVoting = "consumer_double_voting" AttributeDistributionCurrentHeight = "current_distribution_height" AttributeDistributionNextHeight = "next_distribution_height" From f12a5c016f40bd75beb6821b986344802436159b Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Thu, 31 Aug 2023 17:45:56 +0200 Subject: [PATCH 05/12] fix: tiny bug in `NewSubmitConsumerDoubleVotingCmd` (#1247) * fix double voting cli * fix bug double signing handler * godoc * nits * revert wrong push of lasts commits --- x/ccv/provider/client/cli/tx.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/ccv/provider/client/cli/tx.go b/x/ccv/provider/client/cli/tx.go index 1d3ccc0011..a4b6e233e0 100644 --- a/x/ccv/provider/client/cli/tx.go +++ b/x/ccv/provider/client/cli/tx.go @@ -153,7 +153,7 @@ Examples: func NewSubmitConsumerDoubleVotingCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "submit consumer-double-voting [evidence] [infraction_header]", + Use: "submit-consumer-double-voting [evidence] [infraction_header]", Short: "submit a double voting evidence for a consumer chain", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { @@ -176,7 +176,7 @@ func NewSubmitConsumerDoubleVotingCmd() *cobra.Command { return err } - msg, err := types.NewMsgSubmitConsumerDoubleVoting(submitter, ev, nil) + msg, err := types.NewMsgSubmitConsumerDoubleVoting(submitter, ev, &header) if err != nil { return err } From 2501e83e9678f16bda19ce2187dda7abaa806eed Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Mon, 4 Sep 2023 16:57:43 +0200 Subject: [PATCH 06/12] fix: make `HandleConsumerDoubleVoting` works with provider pubkeys (#1254) * fix double voting cli * fix bug double signing handler * godoc * nits * lint * nit --- tests/integration/double_vote.go | 104 ++++++++++++++++------ x/ccv/provider/keeper/double_vote.go | 59 ++++++++---- x/ccv/provider/keeper/double_vote_test.go | 12 +-- x/ccv/provider/types/msg.go | 12 ++- 4 files changed, 137 insertions(+), 50 deletions(-) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index 04d976ed61..184f7604bb 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -19,32 +19,58 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { s.setDefaultValSigningInfo(*v) } - valSet, err := tmtypes.ValidatorSetFromProto(s.consumerChain.LastHeader.ValidatorSet) + consuValSet, err := tmtypes.ValidatorSetFromProto(s.consumerChain.LastHeader.ValidatorSet) s.Require().NoError(err) + consuVal := consuValSet.Validators[0] + s.Require().NoError(err) + consuSigner := s.consumerChain.Signers[consuVal.Address.String()] - val := valSet.Validators[0] - signer := s.consumerChain.Signers[val.Address.String()] + provValSet, err := tmtypes.ValidatorSetFromProto(s.providerChain.LastHeader.ValidatorSet) + s.Require().NoError(err) + provVal := provValSet.Validators[0] + provSigner := s.providerChain.Signers[provVal.Address.String()] blockID1 := testutil.MakeBlockID([]byte("blockhash"), 1000, []byte("partshash")) blockID2 := testutil.MakeBlockID([]byte("blockhash2"), 1000, []byte("partshash")) // Note that votes are signed along with the chain ID // see VoteSignBytes in https://github.com/cometbft/cometbft/blob/main/types/vote.go#L139 - vote1 := testutil.MakeAndSignVote( + + // create two votes using the consumer validator key + consuVote := testutil.MakeAndSignVote( + blockID1, + s.consumerCtx().BlockHeight(), + s.consumerCtx().BlockTime(), + consuValSet, + consuSigner, + s.consumerChain.ChainID, + ) + + consuBadVote := testutil.MakeAndSignVote( + blockID2, + s.consumerCtx().BlockHeight(), + s.consumerCtx().BlockTime(), + consuValSet, + consuSigner, + s.consumerChain.ChainID, + ) + + // create two votes using the provider validator key + provVote := testutil.MakeAndSignVote( blockID1, s.consumerCtx().BlockHeight(), s.consumerCtx().BlockTime(), - valSet, - signer, + provValSet, + provSigner, s.consumerChain.ChainID, ) - badVote := testutil.MakeAndSignVote( + provBadVote := testutil.MakeAndSignVote( blockID2, s.consumerCtx().BlockHeight(), s.consumerCtx().BlockTime(), - valSet, - signer, + provValSet, + provSigner, s.consumerChain.ChainID, ) @@ -57,10 +83,10 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { { "invalid consumer chain id - shouldn't pass", &tmtypes.DuplicateVoteEvidence{ - VoteA: vote1, - VoteB: badVote, - ValidatorPower: val.VotingPower, - TotalVotingPower: val.VotingPower, + VoteA: consuVote, + VoteB: consuBadVote, + ValidatorPower: consuVal.VotingPower, + TotalVotingPower: consuVal.VotingPower, Timestamp: s.consumerCtx().BlockTime(), }, "chainID", @@ -68,12 +94,12 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { }, { // create an invalid evidence containing two identical votes - "invalid double voting evidence - shouldn't pass", + "invalid double voting evidence with identical votes - shouldn't pass", &tmtypes.DuplicateVoteEvidence{ - VoteA: vote1, - VoteB: vote1, - ValidatorPower: val.VotingPower, - TotalVotingPower: val.VotingPower, + VoteA: consuVote, + VoteB: consuVote, + ValidatorPower: consuVal.VotingPower, + TotalVotingPower: consuVal.VotingPower, Timestamp: s.consumerCtx().BlockTime(), }, s.consumerChain.ChainID, @@ -84,12 +110,25 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { // we create two votes that only differ by their Block IDs and // signed them using the same validator private key and chain ID // of the consumer chain - "valid double voting evidence - should pass", + "valid double voting evidence 1 - should pass", &tmtypes.DuplicateVoteEvidence{ - VoteA: vote1, - VoteB: badVote, - ValidatorPower: val.VotingPower, - TotalVotingPower: val.VotingPower, + VoteA: consuVote, + VoteB: consuBadVote, + ValidatorPower: consuVal.VotingPower, + TotalVotingPower: consuVal.VotingPower, + Timestamp: s.consumerCtx().BlockTime(), + }, + s.consumerChain.ChainID, + true, + }, + { + // create a double voting evidence using the provider validator key + "valid double voting evidence 2 - should pass", + &tmtypes.DuplicateVoteEvidence{ + VoteA: provVote, + VoteB: provBadVote, + ValidatorPower: consuVal.VotingPower, + TotalVotingPower: consuVal.VotingPower, Timestamp: s.consumerCtx().BlockTime(), }, s.consumerChain.ChainID, @@ -97,26 +136,37 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { }, } - consuAddr := types.NewConsumerConsAddress(sdk.ConsAddress(val.Address.Bytes())) + consuAddr := types.NewConsumerConsAddress(sdk.ConsAddress(consuVal.Address.Bytes())) provAddr := s.providerApp.GetProviderKeeper().GetProviderAddrFromConsumerAddr(s.providerCtx(), s.consumerChain.ChainID, consuAddr) for _, tc := range testCases { s.Run(tc.name, func() { + // reset context for each run + provCtx := s.providerCtx() + + // if the evidence was built using the validator provider address and key, + // we remove the consumer key assigned to the validator otherwise + // HandleConsumerDoubleVoting uses the consumer key to verify the signature + if tc.ev.VoteA.ValidatorAddress.String() != consuVal.Address.String() { + s.providerApp.GetProviderKeeper().DeleteKeyAssignments(provCtx, s.consumerChain.ChainID) + } + err = s.providerApp.GetProviderKeeper().HandleConsumerDoubleVoting( - s.providerCtx(), + provCtx, tc.ev, tc.chainID, ) + if tc.expPass { s.Require().NoError(err) // verifies that the jailing has occurred - s.Require().True(s.providerApp.GetTestStakingKeeper().IsValidatorJailed(s.providerCtx(), provAddr.ToSdkConsAddr())) + s.Require().True(s.providerApp.GetTestStakingKeeper().IsValidatorJailed(provCtx, provAddr.ToSdkConsAddr())) } else { s.Require().Error(err) // verifies that no jailing and has occurred - s.Require().False(s.providerApp.GetTestStakingKeeper().IsValidatorJailed(s.providerCtx(), provAddr.ToSdkConsAddr())) + s.Require().False(s.providerApp.GetTestStakingKeeper().IsValidatorJailed(provCtx, provAddr.ToSdkConsAddr())) } }) } diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go index ee47e233ee..171be6b250 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -5,11 +5,12 @@ import ( "fmt" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" ccvtypes "github.com/cosmos/interchain-security/v2/x/ccv/types" - tmprotocrypto "github.com/tendermint/tendermint/proto/tendermint/crypto" tmtypes "github.com/tendermint/tendermint/types" ) @@ -23,14 +24,14 @@ func (k Keeper) HandleConsumerDoubleVoting(ctx sdk.Context, evidence *tmtypes.Du types.NewConsumerConsAddress(sdk.ConsAddress(evidence.VoteA.ValidatorAddress.Bytes())), ) - // get the consumer chain public key assigned to the validator - consuPubkey, ok := k.GetValidatorConsumerPubKey(ctx, chainID, providerAddr) - if !ok { - return fmt.Errorf("cannot find public key for validator %s and consumer chain %s", providerAddr.String(), chainID) + // get validator pubkey used on the consumer chain + pubkey, err := k.getValidatorPubkeyOnConsumer(ctx, chainID, providerAddr) + if err != nil { + return err } // verifies the double voting evidence using the consumer chain public key - if err := k.VerifyDoubleVotingEvidence(ctx, *evidence, chainID, consuPubkey); err != nil { + if err := k.VerifyDoubleVotingEvidence(ctx, *evidence, chainID, pubkey); err != nil { return err } @@ -39,7 +40,7 @@ func (k Keeper) HandleConsumerDoubleVoting(ctx sdk.Context, evidence *tmtypes.Du k.Logger(ctx).Info( "confirmed equivocation", - "byzantine validator address", providerAddr, + "byzantine validator address", providerAddr.String(), ) return nil @@ -51,8 +52,12 @@ func (k Keeper) VerifyDoubleVotingEvidence( ctx sdk.Context, evidence tmtypes.DuplicateVoteEvidence, chainID string, - pubkey tmprotocrypto.PublicKey, + pubkey cryptotypes.PubKey, ) error { + if pubkey == nil { + return fmt.Errorf("validator public key cannot be empty") + } + // Note that since we're only jailing validators for double voting on a consumer chain, // the age of the evidence is irrelevant and therefore isn't checked. @@ -89,21 +94,45 @@ func (k Keeper) VerifyDoubleVotingEvidence( ) } - pk, err := cryptocodec.FromTmProtoPublicKey(pubkey) - if err != nil { - return err - } - va := evidence.VoteA.ToProto() vb := evidence.VoteB.ToProto() // signatures must be valid - if !pk.VerifySignature(tmtypes.VoteSignBytes(chainID, va), evidence.VoteA.Signature) { + if !pubkey.VerifySignature(tmtypes.VoteSignBytes(chainID, va), evidence.VoteA.Signature) { return fmt.Errorf("verifying VoteA: %w", tmtypes.ErrVoteInvalidSignature) } - if !pk.VerifySignature(tmtypes.VoteSignBytes(chainID, vb), evidence.VoteB.Signature) { + if !pubkey.VerifySignature(tmtypes.VoteSignBytes(chainID, vb), evidence.VoteB.Signature) { return fmt.Errorf("verifying VoteB: %w", tmtypes.ErrVoteInvalidSignature) } return nil } + +// getValidatorPubkeyOnConsumer returns the public key a validator used on a given consumer chain. +// Note that it can either be an assigned public key or the same public key the validator uses +// on the provider chain. +func (k Keeper) getValidatorPubkeyOnConsumer( + ctx sdk.Context, + chainID string, + providerAddr types.ProviderConsAddress, +) (pubkey cryptotypes.PubKey, err error) { + tmPK, ok := k.GetValidatorConsumerPubKey(ctx, chainID, providerAddr) + if ok { + pubkey, err = cryptocodec.FromTmProtoPublicKey(tmPK) + if err != nil { + return + } + } else { + val, ok := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) + if !ok { + err = fmt.Errorf("cannot find validator %s", providerAddr.String()) + return + } + pubkey, err = val.ConsPubKey() + if err != nil { + return + } + } + + return +} diff --git a/x/ccv/provider/keeper/double_vote_test.go b/x/ccv/provider/keeper/double_vote_test.go index 9bb789c381..cc8280b14d 100644 --- a/x/ccv/provider/keeper/double_vote_test.go +++ b/x/ccv/provider/keeper/double_vote_test.go @@ -4,11 +4,11 @@ import ( "testing" "time" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" testutil "github.com/cosmos/interchain-security/v2/testutil/crypto" testkeeper "github.com/cosmos/interchain-security/v2/testutil/keeper" "github.com/stretchr/testify/require" - tmencoding "github.com/tendermint/tendermint/crypto/encoding" - tmprotocrypto "github.com/tendermint/tendermint/proto/tendermint/crypto" tmtypes "github.com/tendermint/tendermint/types" ) @@ -31,17 +31,17 @@ func TestVerifyDoubleVotingEvidence(t *testing.T) { ctx = ctx.WithBlockTime(time.Now()) - valPubkey1, err := tmencoding.PubKeyToProto(val1.PubKey) + valPubkey1, err := cryptocodec.FromTmPubKeyInterface(val1.PubKey) require.NoError(t, err) - valPubkey2, err := tmencoding.PubKeyToProto(val2.PubKey) + valPubkey2, err := cryptocodec.FromTmPubKeyInterface(val2.PubKey) require.NoError(t, err) testCases := []struct { name string votes []*tmtypes.Vote chainID string - pubkey tmprotocrypto.PublicKey + pubkey cryptotypes.PubKey expPass bool }{ { @@ -209,7 +209,7 @@ func TestVerifyDoubleVotingEvidence(t *testing.T) { ), }, chainID, - tmprotocrypto.PublicKey{}, + nil, false, }, { diff --git a/x/ccv/provider/types/msg.go b/x/ccv/provider/types/msg.go index e5dbb16697..b6741a0a43 100644 --- a/x/ccv/provider/types/msg.go +++ b/x/ccv/provider/types/msg.go @@ -210,8 +210,16 @@ func (msg MsgSubmitConsumerDoubleVoting) ValidateBasic() error { return fmt.Errorf("double voting evidence cannot be nil") } - if msg.InfractionBlockHeader.Header == nil { - return fmt.Errorf("infraction header cannot be nil") + if msg.InfractionBlockHeader == nil { + return fmt.Errorf("infraction block header cannot be nil") + } + + if msg.InfractionBlockHeader.SignedHeader == nil { + return fmt.Errorf("signed header in infraction block header cannot be nil") + } + + if msg.InfractionBlockHeader.SignedHeader.Header == nil { + return fmt.Errorf("invalid signed header in infraction block header, 'SignedHeader.Header' is nil") } return nil From eb6a0790ca9b220c1881e6f64c8e5d9dabcd1667 Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Wed, 6 Sep 2023 16:50:24 +0200 Subject: [PATCH 07/12] fix: verify equivocation using validator pubkey in `SubmitConsumerDoubleVoting` msg (#1264) * verify dv evidence using malicious validator pubkey in infraction block header * nits * nits --- tests/integration/double_vote.go | 25 ++++++++++++ x/ccv/provider/keeper/double_vote.go | 57 +++++++--------------------- x/ccv/provider/keeper/msg_server.go | 31 ++++++++++++++- 3 files changed, 67 insertions(+), 46 deletions(-) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index 184f7604bb..c79b92115e 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -1,9 +1,11 @@ package integration import ( + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdk "github.com/cosmos/cosmos-sdk/types" testutil "github.com/cosmos/interchain-security/v2/testutil/crypto" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" + "github.com/tendermint/tendermint/crypto" tmtypes "github.com/tendermint/tendermint/types" ) @@ -78,6 +80,7 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { name string ev *tmtypes.DuplicateVoteEvidence chainID string + pubkey crypto.PubKey expPass bool }{ { @@ -90,6 +93,20 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { Timestamp: s.consumerCtx().BlockTime(), }, "chainID", + consuVal.PubKey, + false, + }, + { + "wrong public key - shouldn't pass", + &tmtypes.DuplicateVoteEvidence{ + VoteA: consuVote, + VoteB: consuVote, + ValidatorPower: consuVal.VotingPower, + TotalVotingPower: consuVal.VotingPower, + Timestamp: s.consumerCtx().BlockTime(), + }, + s.consumerChain.ChainID, + provVal.PubKey, false, }, { @@ -103,6 +120,7 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { Timestamp: s.consumerCtx().BlockTime(), }, s.consumerChain.ChainID, + consuVal.PubKey, false, }, { @@ -119,6 +137,7 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { Timestamp: s.consumerCtx().BlockTime(), }, s.consumerChain.ChainID, + consuVal.PubKey, true, }, { @@ -132,6 +151,7 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { Timestamp: s.consumerCtx().BlockTime(), }, s.consumerChain.ChainID, + provVal.PubKey, true, }, } @@ -151,10 +171,15 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { s.providerApp.GetProviderKeeper().DeleteKeyAssignments(provCtx, s.consumerChain.ChainID) } + // convert validator public key + pk, err := cryptocodec.FromTmPubKeyInterface(tc.pubkey) + s.Require().NoError(err) + err = s.providerApp.GetProviderKeeper().HandleConsumerDoubleVoting( provCtx, tc.ev, tc.chainID, + pk, ) if tc.expPass { diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go index 171be6b250..ee5f716ef8 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" - cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -14,9 +13,19 @@ import ( tmtypes "github.com/tendermint/tendermint/types" ) -// HandleConsumerDoubleVoting verifies a double voting evidence for a given a consumer chain and, -// if successful, executes the jailing of the malicious validator. -func (k Keeper) HandleConsumerDoubleVoting(ctx sdk.Context, evidence *tmtypes.DuplicateVoteEvidence, chainID string) error { +// HandleConsumerDoubleVoting verifies a double voting evidence for a given a consumer chain ID +// and a public key and, if successful, executes the jailing of the malicious validator. +func (k Keeper) HandleConsumerDoubleVoting( + ctx sdk.Context, + evidence *tmtypes.DuplicateVoteEvidence, + chainID string, + pubkey cryptotypes.PubKey, +) error { + // verifies the double voting evidence using the consumer chain public key + if err := k.VerifyDoubleVotingEvidence(ctx, *evidence, chainID, pubkey); err != nil { + return err + } + // get the validator's consensus address on the provider providerAddr := k.GetProviderAddrFromConsumerAddr( ctx, @@ -24,17 +33,6 @@ func (k Keeper) HandleConsumerDoubleVoting(ctx sdk.Context, evidence *tmtypes.Du types.NewConsumerConsAddress(sdk.ConsAddress(evidence.VoteA.ValidatorAddress.Bytes())), ) - // get validator pubkey used on the consumer chain - pubkey, err := k.getValidatorPubkeyOnConsumer(ctx, chainID, providerAddr) - if err != nil { - return err - } - - // verifies the double voting evidence using the consumer chain public key - if err := k.VerifyDoubleVotingEvidence(ctx, *evidence, chainID, pubkey); err != nil { - return err - } - // execute the jailing k.JailValidator(ctx, providerAddr) @@ -107,32 +105,3 @@ func (k Keeper) VerifyDoubleVotingEvidence( return nil } - -// getValidatorPubkeyOnConsumer returns the public key a validator used on a given consumer chain. -// Note that it can either be an assigned public key or the same public key the validator uses -// on the provider chain. -func (k Keeper) getValidatorPubkeyOnConsumer( - ctx sdk.Context, - chainID string, - providerAddr types.ProviderConsAddress, -) (pubkey cryptotypes.PubKey, err error) { - tmPK, ok := k.GetValidatorConsumerPubKey(ctx, chainID, providerAddr) - if ok { - pubkey, err = cryptocodec.FromTmProtoPublicKey(tmPK) - if err != nil { - return - } - } else { - val, ok := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) - if !ok { - err = fmt.Errorf("cannot find validator %s", providerAddr.String()) - return - } - pubkey, err = val.ConsPubKey() - if err != nil { - return - } - } - - return -} diff --git a/x/ccv/provider/keeper/msg_server.go b/x/ccv/provider/keeper/msg_server.go index b6b96bb010..9293859b02 100644 --- a/x/ccv/provider/keeper/msg_server.go +++ b/x/ccv/provider/keeper/msg_server.go @@ -4,6 +4,8 @@ import ( "context" "encoding/base64" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -156,8 +158,33 @@ func (k msgServer) SubmitConsumerDoubleVoting(goCtx context.Context, msg *types. return nil, err } - if err := k.Keeper.HandleConsumerDoubleVoting(ctx, evidence, msg.InfractionBlockHeader.Header.ChainID); err != nil { - return &types.MsgSubmitConsumerDoubleVotingResponse{}, err + // parse the validator set of the infraction block header in order + // to find the public key of the validator who double voted + + // get validator set + valset, err := tmtypes.ValidatorSetFromProto(msg.InfractionBlockHeader.ValidatorSet) + if err != nil { + return nil, err + } + + // look for the malicious validator in the validator set + _, validator := valset.GetByAddress(evidence.VoteA.ValidatorAddress) + if validator == nil { + return nil, errorsmod.Wrapf( + ccvtypes.ErrInvalidEvidence, + "misbehaving validator %s cannot be found in the infraction block header validator set", + evidence.VoteA.ValidatorAddress) + } + + pubkey, err := cryptocodec.FromTmPubKeyInterface(validator.PubKey) + if err != nil { + return nil, err + } + + // handle the double voting evidence using the chain ID of the infraction block header + // and the malicious validator's public key + if err := k.Keeper.HandleConsumerDoubleVoting(ctx, evidence, msg.InfractionBlockHeader.Header.ChainID, pubkey); err != nil { + return nil, err } ctx.EventManager().EmitEvents(sdk.Events{ From 98af9c0144bb3ceb84acc195dcfbf16c2afb9a23 Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Fri, 8 Sep 2023 16:13:13 +0200 Subject: [PATCH 08/12] refactor: update the E2E tests to work with Hermes relayer v1.6.0 (#1278) * save changes * fix hermes config * fist successful run * nit * nits * nits * doc and nits * lint --- tests/e2e/actions.go | 24 ++++++++++- tests/e2e/actions_consumer_misbehaviour.go | 7 ++- tests/e2e/main.go | 2 + tests/e2e/state.go | 48 +++++++++++++++++++++ tests/e2e/steps_consumer_misbehaviour.go | 50 ++++++++++++++++------ tests/e2e/testnet-scripts/fork-consumer.sh | 6 ++- 6 files changed, 119 insertions(+), 18 deletions(-) diff --git a/tests/e2e/actions.go b/tests/e2e/actions.go index 9c7d41af17..0c47b452e2 100644 --- a/tests/e2e/actions.go +++ b/tests/e2e/actions.go @@ -718,7 +718,7 @@ rpc_addr = "%s" rpc_timeout = "10s" store_prefix = "ibc" trusting_period = "14days" -websocket_addr = "%s" +event_source = { mode = "push", url = "%s", batch_delay = "50ms" } ccv_consumer_chain = %v [chains.gas_price] @@ -1844,3 +1844,25 @@ func (tr TestRun) GetPathNameForGorelayer(chainA, chainB chainID) string { return pathName } + +// Run an instance of the Hermes relayer using the "evidence" command, +// which detects evidences committed to the blocks of a consumer chain. +// Each infraction detected is reported to the provider chain using +// either a SubmitConsumerDoubleVoting or a SubmitConsumerMisbehaviour message. +type detectConsumerEvidenceAction struct { + chain chainID +} + +func (tr TestRun) detectConsumerEvidence( + action detectConsumerEvidenceAction, + verbose bool, +) { + chainConfig := tr.chainConfigs[action.chain] + //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. + bz, err := exec.Command("docker", "exec", "-d", tr.containerConfig.instanceName, + "hermes", "evidence", "--chain", string(chainConfig.chainId)).CombinedOutput() + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + tr.waitBlocks("provi", 10, 2*time.Minute) +} diff --git a/tests/e2e/actions_consumer_misbehaviour.go b/tests/e2e/actions_consumer_misbehaviour.go index 84eb93152c..0da2f9e56f 100644 --- a/tests/e2e/actions_consumer_misbehaviour.go +++ b/tests/e2e/actions_consumer_misbehaviour.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os/exec" + "strconv" "time" ) @@ -61,6 +62,7 @@ func (tr TestRun) forkConsumerChain(action forkConsumerChainAction, verbose bool } type updateLightClientAction struct { + chain chainID hostChain chainID relayerConfig string clientID string @@ -70,7 +72,9 @@ func (tr TestRun) updateLightClient( action updateLightClientAction, verbose bool, ) { - // hermes clear packets ibc0 transfer channel-13 + // retrieve a trusted height of the consumer light client + trustedHeight := tr.getTrustedHeight(action.hostChain, action.clientID, 2) + //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. cmd := exec.Command("docker", "exec", tr.containerConfig.instanceName, "hermes", "--config", action.relayerConfig, @@ -78,6 +82,7 @@ func (tr TestRun) updateLightClient( "client", "--client", action.clientID, "--host-chain", string(action.hostChain), + "--trusted-height", strconv.Itoa(int(trustedHeight.RevisionHeight)), ) if verbose { log.Println("updateLightClientAction cmd:", cmd.String()) diff --git a/tests/e2e/main.go b/tests/e2e/main.go index 406a015e63..84a06eeab9 100644 --- a/tests/e2e/main.go +++ b/tests/e2e/main.go @@ -180,6 +180,8 @@ func (tr *TestRun) runStep(step Step, verbose bool) { tr.updateLightClient(action, verbose) case assertChainIsHaltedAction: tr.assertChainIsHalted(action, verbose) + case detectConsumerEvidenceAction: + tr.detectConsumerEvidence(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 8d9ba9a81e..d0d908a494 100644 --- a/tests/e2e/state.go +++ b/tests/e2e/state.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "fmt" "log" "os/exec" @@ -776,3 +777,50 @@ func (tr TestRun) getClientFrozenHeight(chain chainID, clientID string) clientty return clienttypes.Height{RevisionHeight: uint64(revHeight), RevisionNumber: uint64(revNumber)} } + +func (tr TestRun) getTrustedHeight( + chain chainID, + clientID string, + index int, +) clienttypes.Height { + //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. + configureNodeCmd := exec.Command("docker", "exec", tr.containerConfig.instanceName, "hermes", + "--json", "query", "client", "consensus", "--chain", string(chain), + `--client`, clientID, + ) + + 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) + + var trustedHeight gjson.Result + // iterate on the relayer's response + // and parse the the command "result" + for scanner.Scan() { + out := scanner.Text() + if len(gjson.Get(out, "result").Array()) > 0 { + trustedHeight = gjson.Get(out, "result").Array()[index] + break + } + } + + revHeight, err := strconv.Atoi(trustedHeight.Get("revision_height").String()) + if err != nil { + log.Fatal(err) + } + + revNumber, err := strconv.Atoi(trustedHeight.Get("revision_number").String()) + if err != nil { + log.Fatal(err) + } + return clienttypes.Height{RevisionHeight: uint64(revHeight), RevisionNumber: uint64(revNumber)} +} diff --git a/tests/e2e/steps_consumer_misbehaviour.go b/tests/e2e/steps_consumer_misbehaviour.go index 6401b5f638..53cfb78fae 100644 --- a/tests/e2e/steps_consumer_misbehaviour.go +++ b/tests/e2e/steps_consumer_misbehaviour.go @@ -213,43 +213,65 @@ func stepsCauseConsumerMisbehaviour(consumerName string) []Step { }, state: State{}, }, + // start relayer to detect IBC misbehaviour { - // start relayer to detect ICS misbehaviour action: startRelayerAction{}, state: State{}, }, + // detect the ICS misbehaviour + // and jail alice on the provider + { + action: detectConsumerEvidenceAction{ + chain: chainID(consumerName), + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 20, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 20, + }, + }, + }, + }, { // update the fork consumer client to create a light client attack // which should trigger a ICS misbehaviour message action: updateLightClientAction{ + chain: chainID(consumerName), + clientID: consumerClientID, hostChain: chainID("provi"), relayerConfig: forkRelayerConfig, // this relayer config uses the "forked" consumer - clientID: consumerClientID, }, state: State{ chainID("provi"): ChainState{ - // validator should be jailed on the provider + // alice should be jailed on the provider ValPowers: &map[validatorID]uint{ validatorID("alice"): 0, validatorID("bob"): 20, }, - // The consumer light client should not be frozen + // The consumer light client should be frozen on the provider ClientsFrozenHeights: &map[string]clienttypes.Height{ - "07-tendermint-0": { + consumerClientID: { RevisionNumber: 0, - RevisionHeight: 0, + RevisionHeight: 1, }, }, }, + chainID(consumerName): ChainState{ + // consumer should not have learned the jailing of alice + // since its light client is frozen on the provider + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 20, + }, + }, }, }, - // we expect the consumer chain to be halted since the last VSC packet should - // have updated the alice validator power to 0. - { - action: assertChainIsHaltedAction{ - chain: chainID("consu"), - }, - state: State{}, - }, } } diff --git a/tests/e2e/testnet-scripts/fork-consumer.sh b/tests/e2e/testnet-scripts/fork-consumer.sh index 0bf96fcb79..7c12438b71 100644 --- a/tests/e2e/testnet-scripts/fork-consumer.sh +++ b/tests/e2e/testnet-scripts/fork-consumer.sh @@ -63,7 +63,7 @@ rpc_addr = "http://$CONS_CHAIN_PREFIX.252:26658" rpc_timeout = "10s" store_prefix = "ibc" trusting_period = "2days" -websocket_addr = "ws://$CONS_CHAIN_PREFIX.252:26658/websocket" +event_source = { mode = 'push', url = 'ws://$CONS_CHAIN_PREFIX.252:26658/websocket' , batch_delay = '50ms' } [chains.gas_price] denom = "stake" @@ -85,7 +85,9 @@ rpc_addr = "http://$PROV_CHAIN_PREFIX.4:26658" rpc_timeout = "10s" store_prefix = "ibc" trusting_period = "2days" -websocket_addr = "ws://$PROV_CHAIN_PREFIX.4:26658/websocket" +event_source = { mode = 'push', url = 'ws://$PROV_CHAIN_PREFIX.4:26658/websocket' , batch_delay = '50ms' } + + [chains.gas_price] denom = "stake" From c881a1aad37f2f8041c913468602edaf69fef9bf Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Mon, 11 Sep 2023 18:21:15 +0200 Subject: [PATCH 09/12] test: add E2E tests for double voting evidence handling (#1256) * fix double voting cli * add double-signing e2e test * refortmat e2e double voting test * godoc, revert unwanted changes * nit * verify dv evidence using malicious validator pubkey in infraction block header * save changes * fix hermes config * fist successful run * nit * nits * nits * doc and nits * lint * refactor * typo * change hermes docker image * nits * Update tests/e2e/steps.go Co-authored-by: Philip Offtermatt <57488781+p-offtermatt@users.noreply.github.com> * address PR comments * nits --------- Co-authored-by: Philip Offtermatt <57488781+p-offtermatt@users.noreply.github.com> --- tests/e2e/main.go | 2 + tests/e2e/steps.go | 10 ++++- tests/e2e/steps_double_sign.go | 76 ++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/tests/e2e/main.go b/tests/e2e/main.go index 84a06eeab9..ac9becdd5e 100644 --- a/tests/e2e/main.go +++ b/tests/e2e/main.go @@ -63,7 +63,9 @@ func main() { {DemocracyTestRun(false), rewardDenomConsumerSteps}, {SlashThrottleTestRun(), slashThrottleSteps}, {ConsumerMisbehaviourTestRun(), consumerMisbehaviourSteps}, + {DefaultTestRun(), consumerDoubleSignSteps}, } + if includeMultiConsumer != nil && *includeMultiConsumer { testRuns = append(testRuns, testRunWithSteps{MultiConsumerTestRun(), multipleConsumers}) } diff --git a/tests/e2e/steps.go b/tests/e2e/steps.go index 78a56654e6..fb506eff5f 100644 --- a/tests/e2e/steps.go +++ b/tests/e2e/steps.go @@ -91,6 +91,14 @@ var changeoverSteps = concatSteps( var consumerMisbehaviourSteps = concatSteps( // start provider and consumer chain stepsStartChainsWithSoftOptOut("consu"), - // make consumer validator to misbehave and get jail + // make a consumer validator to misbehave and get jailed stepsCauseConsumerMisbehaviour("consu"), ) + +var consumerDoubleSignSteps = concatSteps( + // start provider and consumer chain + stepsStartChains([]string{"consu"}, false), + + // make a consumer validator double sign and get jailed + stepsCauseDoubleSignOnConsumer("consu", "provi"), +) diff --git a/tests/e2e/steps_double_sign.go b/tests/e2e/steps_double_sign.go index c007fa5c1c..dcce236b46 100644 --- a/tests/e2e/steps_double_sign.go +++ b/tests/e2e/steps_double_sign.go @@ -128,3 +128,79 @@ func stepsDoubleSignOnProviderAndConsumer(consumerName string) []Step { }, } } + +// Steps that make bob double sign on the consumer +func stepsCauseDoubleSignOnConsumer(consumerName, providerName string) []Step { + return []Step{ + { + action: doublesignSlashAction{ + chain: chainID(consumerName), + validator: validatorID("bob"), + }, + state: State{ + chainID(providerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 500, + validatorID("bob"): 500, + validatorID("carol"): 500, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 500, + validatorID("bob"): 500, + validatorID("carol"): 500, + }, + }, + }, + }, + // detect the double voting infraction + // and jail bob on the provider + { + action: detectConsumerEvidenceAction{ + chain: chainID(consumerName), + }, + state: State{ + chainID(providerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 500, + validatorID("bob"): 0, + validatorID("carol"): 500, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 500, + validatorID("bob"): 500, + validatorID("carol"): 500, + }, + }, + }, + }, + // consumer learns about the jailing + { + action: relayPacketsAction{ + chainA: chainID(providerName), + chainB: chainID(consumerName), + port: "provider", + channel: 0, + }, + state: State{ + chainID(providerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 500, + validatorID("bob"): 0, + validatorID("carol"): 500, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 500, + validatorID("bob"): 0, + validatorID("carol"): 500, + }, + }, + }, + }, + } +} From a71f1feb8156ab4e01d00d91a83a4350d5d7147a Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Wed, 13 Sep 2023 17:24:15 +0200 Subject: [PATCH 10/12] save --- proto/interchain_security/ccv/provider/v1/tx.proto | 8 ++++---- tests/e2e/actions.go | 10 +++++----- tests/e2e/actions_consumer_misbehaviour.go | 2 ++ tests/e2e/main.go | 4 ++-- tests/e2e/steps_consumer_misbehaviour.go | 6 +++--- tests/e2e/steps_double_sign.go | 2 +- x/ccv/provider/client/cli/tx.go | 12 +++++++++++- x/ccv/provider/keeper/double_vote.go | 5 +---- x/ccv/provider/keeper/misbehaviour.go | 4 ++-- x/ccv/provider/types/codec.go | 5 +++++ 10 files changed, 36 insertions(+), 22 deletions(-) diff --git a/proto/interchain_security/ccv/provider/v1/tx.proto b/proto/interchain_security/ccv/provider/v1/tx.proto index 64e4c88b70..12187ac9b5 100644 --- a/proto/interchain_security/ccv/provider/v1/tx.proto +++ b/proto/interchain_security/ccv/provider/v1/tx.proto @@ -49,8 +49,8 @@ message MsgRegisterConsumerRewardDenom { // MsgRegisterConsumerRewardDenomResponse defines the Msg/RegisterConsumerRewardDenom response type. message MsgRegisterConsumerRewardDenomResponse {} -// MsgSubmitConsumerMisbehaviour defines a message that reports a misbehaviour -// observed on a consumer chain +// MsgSubmitConsumerMisbehaviour defines a message that reports a light client attack, +// also known as misbehaviour, observed on a consumer chain // Note that the misbheaviour' headers must contain the same trusted states message MsgSubmitConsumerMisbehaviour { option (gogoproto.equal) = false; @@ -64,8 +64,8 @@ message MsgSubmitConsumerMisbehaviour { message MsgSubmitConsumerMisbehaviourResponse {} -// MsgSubmitConsumerDoubleVoting defines a message that reports an equivocation -// observed on a consumer chain +// MsgSubmitConsumerDoubleVoting defines a message that reports +// a double signing infraction observed on a consumer chain message MsgSubmitConsumerDoubleVoting { option (gogoproto.equal) = false; option (gogoproto.goproto_getters) = false; diff --git a/tests/e2e/actions.go b/tests/e2e/actions.go index 0c47b452e2..20740d7196 100644 --- a/tests/e2e/actions.go +++ b/tests/e2e/actions.go @@ -524,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 { @@ -841,7 +841,6 @@ func (tr TestRun) addChainToHermes( 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, @@ -1849,15 +1848,16 @@ func (tr TestRun) GetPathNameForGorelayer(chainA, chainB chainID) string { // which detects evidences committed to the blocks of a consumer chain. // Each infraction detected is reported to the provider chain using // either a SubmitConsumerDoubleVoting or a SubmitConsumerMisbehaviour message. -type detectConsumerEvidenceAction struct { +type startConsumerEvidenceDetectorAction struct { chain chainID } -func (tr TestRun) detectConsumerEvidence( - action detectConsumerEvidenceAction, +func (tr TestRun) startConsumerEvidenceDetector( + action startConsumerEvidenceDetectorAction, verbose bool, ) { chainConfig := tr.chainConfigs[action.chain] + // run in detached mode so it will keep running in the background //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. bz, err := exec.Command("docker", "exec", "-d", tr.containerConfig.instanceName, "hermes", "evidence", "--chain", string(chainConfig.chainId)).CombinedOutput() diff --git a/tests/e2e/actions_consumer_misbehaviour.go b/tests/e2e/actions_consumer_misbehaviour.go index 0da2f9e56f..b5c9efbc2a 100644 --- a/tests/e2e/actions_consumer_misbehaviour.go +++ b/tests/e2e/actions_consumer_misbehaviour.go @@ -9,6 +9,8 @@ import ( "time" ) +// forkConsumerChainAction forks the consumer chain by cloning of a validator node +// Note that the chain fork is running in an different network type forkConsumerChainAction struct { consumerChain chainID providerChain chainID diff --git a/tests/e2e/main.go b/tests/e2e/main.go index ac9becdd5e..be868a8100 100644 --- a/tests/e2e/main.go +++ b/tests/e2e/main.go @@ -182,8 +182,8 @@ func (tr *TestRun) runStep(step Step, verbose bool) { tr.updateLightClient(action, verbose) case assertChainIsHaltedAction: tr.assertChainIsHalted(action, verbose) - case detectConsumerEvidenceAction: - tr.detectConsumerEvidence(action, verbose) + case startConsumerEvidenceDetectorAction: + tr.startConsumerEvidenceDetector(action, verbose) default: log.Fatalf("unknown action in testRun %s: %#v", tr.name, action) } diff --git a/tests/e2e/steps_consumer_misbehaviour.go b/tests/e2e/steps_consumer_misbehaviour.go index 53cfb78fae..92402d6dc6 100644 --- a/tests/e2e/steps_consumer_misbehaviour.go +++ b/tests/e2e/steps_consumer_misbehaviour.go @@ -204,7 +204,7 @@ func stepsCauseConsumerMisbehaviour(consumerName string) []Step { forkRelayerConfig := "/root/.hermes/config_fork.toml" return []Step{ { - // fork the consumer chain by cloning of its validator node + // fork the consumer chain by cloning the alice validator node action: forkConsumerChainAction{ consumerChain: chainID(consumerName), providerChain: chainID("provi"), @@ -218,10 +218,10 @@ func stepsCauseConsumerMisbehaviour(consumerName string) []Step { action: startRelayerAction{}, state: State{}, }, - // detect the ICS misbehaviour + // run Hermes relayer instance to detect the ICS misbehaviour // and jail alice on the provider { - action: detectConsumerEvidenceAction{ + action: startConsumerEvidenceDetectorAction{ chain: chainID(consumerName), }, state: State{ diff --git a/tests/e2e/steps_double_sign.go b/tests/e2e/steps_double_sign.go index dcce236b46..2adcac14cb 100644 --- a/tests/e2e/steps_double_sign.go +++ b/tests/e2e/steps_double_sign.go @@ -157,7 +157,7 @@ func stepsCauseDoubleSignOnConsumer(consumerName, providerName string) []Step { // detect the double voting infraction // and jail bob on the provider { - action: detectConsumerEvidenceAction{ + action: startConsumerEvidenceDetectorAction{ chain: chainID(consumerName), }, state: State{ diff --git a/x/ccv/provider/client/cli/tx.go b/x/ccv/provider/client/cli/tx.go index a4b6e233e0..01b62c7181 100644 --- a/x/ccv/provider/client/cli/tx.go +++ b/x/ccv/provider/client/cli/tx.go @@ -155,7 +155,17 @@ func NewSubmitConsumerDoubleVotingCmd() *cobra.Command { cmd := &cobra.Command{ Use: "submit-consumer-double-voting [evidence] [infraction_header]", Short: "submit a double voting evidence for a consumer chain", - Args: cobra.ExactArgs(2), + Long: strings.TrimSpace( + fmt.Sprintf(`Submit a Tendermint duplicated vote evidence detected on a consumer chain along with + an IBC light client header at the infraction height. + The DuplicateVoteEvidence type definition can be found in the Tendermint type messages, + see cometbft/proto/tendermint/types/evidence.proto and the light client header + definition is available in ibc-go/proto/ibc/lightclients/tendermint/v1/tendermint.proto. + +Examples: +%s tx provider submit-consumer-double-voting [path/to/evidence.json] --from node0 --home ../node0 --chain-id $CID +`, version.AppName)), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { clientCtx, err := client.GetClientTxContext(cmd) if err != nil { diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go index ee5f716ef8..d61019e239 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -59,9 +59,6 @@ func (k Keeper) VerifyDoubleVotingEvidence( // Note that since we're only jailing validators for double voting on a consumer chain, // the age of the evidence is irrelevant and therefore isn't checked. - // TODO: check the age of the evidence once we slash - // validators for double voting on a consumer chain - // H/R/S must be the same if evidence.VoteA.Height != evidence.VoteB.Height || evidence.VoteA.Round != evidence.VoteB.Round || @@ -73,7 +70,7 @@ func (k Keeper) VerifyDoubleVotingEvidence( evidence.VoteB.Height, evidence.VoteB.Round, evidence.VoteB.Type) } - // Address must be the same + // Addresses must be the same if !bytes.Equal(evidence.VoteA.ValidatorAddress, evidence.VoteB.ValidatorAddress) { return sdkerrors.Wrapf( ccvtypes.ErrInvalidEvidence, diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 35d9219324..3a40b890e9 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -11,7 +11,7 @@ import ( ) // HandleConsumerMisbehaviour checks if the given IBC misbehaviour corresponds to an equivocation light client attack, -// and in this case, jails and tombstones the Byzantine validators +// and in this case, jails the Byzantine validators func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmtypes.Misbehaviour) error { logger := k.Logger(ctx) @@ -34,7 +34,7 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty provAddrs := make([]types.ProviderConsAddress, len(byzantineValidators)) - // jail and tombstone the Byzantine validators + // jailthe Byzantine validators for _, v := range byzantineValidators { providerAddr := k.GetProviderAddrFromConsumerAddr( ctx, diff --git a/x/ccv/provider/types/codec.go b/x/ccv/provider/types/codec.go index 0ef3c2d296..d0e243723a 100644 --- a/x/ccv/provider/types/codec.go +++ b/x/ccv/provider/types/codec.go @@ -41,6 +41,11 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) { &MsgSubmitConsumerMisbehaviour{}, ) + registry.RegisterImplementations( + (*sdk.Msg)(nil), + &MsgSubmitConsumerDoubleVoting{}, + ) + msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) } From 3be76ada57cb7cdfea3c57e31df9e68ad0f296e3 Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Thu, 14 Sep 2023 09:12:04 +0200 Subject: [PATCH 11/12] fix nits --- Dockerfile | 2 +- .../ccv/provider/v1/tx.proto | 3 +- x/ccv/provider/client/cli/tx.go | 12 +++--- x/ccv/provider/keeper/double_vote.go | 6 +-- x/ccv/provider/keeper/misbehaviour.go | 8 ++-- x/ccv/provider/keeper/msg_server.go | 6 +-- x/ccv/provider/types/msg.go | 5 +++ x/ccv/types/errors.go | 40 +++++++++---------- x/ccv/types/events.go | 1 + 9 files changed, 44 insertions(+), 39 deletions(-) diff --git a/Dockerfile b/Dockerfile index 03939617be..7f58c49632 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ RUN go mod tidy RUN make install # Get Hermes build -FROM otacrew/hermes-ics:latest AS hermes-builder +FROM otacrew/hermes-ics:evidence-cmd AS hermes-builder # Get CometMock FROM informalofftermatt/cometmock:latest as cometmock-builder diff --git a/proto/interchain_security/ccv/provider/v1/tx.proto b/proto/interchain_security/ccv/provider/v1/tx.proto index 12187ac9b5..2c04394658 100644 --- a/proto/interchain_security/ccv/provider/v1/tx.proto +++ b/proto/interchain_security/ccv/provider/v1/tx.proto @@ -50,8 +50,7 @@ message MsgRegisterConsumerRewardDenom { message MsgRegisterConsumerRewardDenomResponse {} // MsgSubmitConsumerMisbehaviour defines a message that reports a light client attack, -// also known as misbehaviour, observed on a consumer chain -// Note that the misbheaviour' headers must contain the same trusted states +// also known as a misbehaviour, observed on a consumer chain message MsgSubmitConsumerMisbehaviour { option (gogoproto.equal) = false; option (gogoproto.goproto_getters) = false; diff --git a/x/ccv/provider/client/cli/tx.go b/x/ccv/provider/client/cli/tx.go index 01b62c7181..946d728153 100644 --- a/x/ccv/provider/client/cli/tx.go +++ b/x/ccv/provider/client/cli/tx.go @@ -156,14 +156,14 @@ func NewSubmitConsumerDoubleVotingCmd() *cobra.Command { Use: "submit-consumer-double-voting [evidence] [infraction_header]", Short: "submit a double voting evidence for a consumer chain", Long: strings.TrimSpace( - fmt.Sprintf(`Submit a Tendermint duplicated vote evidence detected on a consumer chain along with - an IBC light client header at the infraction height. - The DuplicateVoteEvidence type definition can be found in the Tendermint type messages, - see cometbft/proto/tendermint/types/evidence.proto and the light client header - definition is available in ibc-go/proto/ibc/lightclients/tendermint/v1/tendermint.proto. + fmt.Sprintf(`Submit a Tendermint duplicated vote evidence detected on a consumer chain with + the IBC light client header for the infraction height. + The DuplicateVoteEvidence type definition can be found in the Tendermint messages, + , see cometbft/proto/tendermint/types/evidence.proto and the IBC header + definition can be found in the IBC messages, see ibc-go/proto/ibc/lightclients/tendermint/v1/tendermint.proto. Examples: -%s tx provider submit-consumer-double-voting [path/to/evidence.json] --from node0 --home ../node0 --chain-id $CID +%s tx provider submit-consumer-double-voting [path/to/evidence.json] [path/to/infraction_header.json] --from node0 --home ../node0 --chain-id $CID `, version.AppName)), Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go index d61019e239..ccca1967fb 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -64,7 +64,7 @@ func (k Keeper) VerifyDoubleVotingEvidence( evidence.VoteA.Round != evidence.VoteB.Round || evidence.VoteA.Type != evidence.VoteB.Type { return sdkerrors.Wrapf( - ccvtypes.ErrInvalidEvidence, + ccvtypes.ErrInvalidDoubleVotingEvidence, "h/r/s does not match: %d/%d/%v vs %d/%d/%v", evidence.VoteA.Height, evidence.VoteA.Round, evidence.VoteA.Type, evidence.VoteB.Height, evidence.VoteB.Round, evidence.VoteB.Type) @@ -73,7 +73,7 @@ func (k Keeper) VerifyDoubleVotingEvidence( // Addresses must be the same if !bytes.Equal(evidence.VoteA.ValidatorAddress, evidence.VoteB.ValidatorAddress) { return sdkerrors.Wrapf( - ccvtypes.ErrInvalidEvidence, + ccvtypes.ErrInvalidDoubleVotingEvidence, "validator addresses do not match: %X vs %X", evidence.VoteA.ValidatorAddress, evidence.VoteB.ValidatorAddress, @@ -83,7 +83,7 @@ func (k Keeper) VerifyDoubleVotingEvidence( // BlockIDs must be different if evidence.VoteA.BlockID.Equals(evidence.VoteB.BlockID) { return sdkerrors.Wrapf( - ccvtypes.ErrInvalidEvidence, + ccvtypes.ErrInvalidDoubleVotingEvidence, "block IDs are the same (%v) - not a real duplicate vote", evidence.VoteA.BlockID, ) diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 3a40b890e9..f149470118 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -34,7 +34,7 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty provAddrs := make([]types.ProviderConsAddress, len(byzantineValidators)) - // jailthe Byzantine validators + // jail the Byzantine validators for _, v := range byzantineValidators { providerAddr := k.GetProviderAddrFromConsumerAddr( ctx, @@ -125,15 +125,15 @@ func (k Keeper) CheckMisbehaviour(ctx sdk.Context, misbehaviour ibctmtypes.Misbe // Check that the headers are at the same height to ensure that // the misbehaviour is for a light client attack and not a time violation, - // https://github.com/cosmos/ibc-go/blob/v4.2.0/modules/light-clients/07-tendermint/types/misbehaviour_handle.go#L53-L58 + // see ibc-go/modules/light-clients/07-tendermint/types/misbehaviour_handle.go if !misbehaviour.Header1.GetHeight().EQ(misbehaviour.Header2.GetHeight()) { return sdkerrors.Wrap(ibcclienttypes.ErrInvalidMisbehaviour, "headers are not at same height") } // CheckMisbehaviourAndUpdateState verifies the misbehaviour against the trusted consensus states // but does NOT update the light client state. - // Note that the CometBFT CheckMisbehaviourAndUpdateState method returns an error if the trusted consensus states are expired, - // see https://github.com/cosmos/ibc-go/blob/v4.2.0/modules/light-clients/07-tendermint/types/misbehaviour_handle.go#L120 + // Note that the IBC CheckMisbehaviourAndUpdateState method returns an error if the trusted consensus states are expired, + // see ibc-go/modules/light-clients/07-tendermint/types/misbehaviour_handle.go _, err := clientState.CheckMisbehaviourAndUpdateState(ctx, k.cdc, clientStore, &misbehaviour) if err != nil { return err diff --git a/x/ccv/provider/keeper/msg_server.go b/x/ccv/provider/keeper/msg_server.go index 9293859b02..83a2eea162 100644 --- a/x/ccv/provider/keeper/msg_server.go +++ b/x/ccv/provider/keeper/msg_server.go @@ -133,7 +133,7 @@ func (k msgServer) RegisterConsumerRewardDenom(goCtx context.Context, msg *types func (k msgServer) SubmitConsumerMisbehaviour(goCtx context.Context, msg *types.MsgSubmitConsumerMisbehaviour) (*types.MsgSubmitConsumerMisbehaviourResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) if err := k.Keeper.HandleConsumerMisbehaviour(ctx, *msg.Misbehaviour); err != nil { - return &types.MsgSubmitConsumerMisbehaviourResponse{}, err + return nil, err } ctx.EventManager().EmitEvents(sdk.Events{ @@ -171,7 +171,7 @@ func (k msgServer) SubmitConsumerDoubleVoting(goCtx context.Context, msg *types. _, validator := valset.GetByAddress(evidence.VoteA.ValidatorAddress) if validator == nil { return nil, errorsmod.Wrapf( - ccvtypes.ErrInvalidEvidence, + ccvtypes.ErrInvalidDoubleVotingEvidence, "misbehaving validator %s cannot be found in the infraction block header validator set", evidence.VoteA.ValidatorAddress) } @@ -189,7 +189,7 @@ func (k msgServer) SubmitConsumerDoubleVoting(goCtx context.Context, msg *types. ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( - ccvtypes.EventTypeSubmitConsumerMisbehaviour, + ccvtypes.EventTypeSubmitConsumerDoubleVoting, sdk.NewAttribute(ccvtypes.AttributeConsumerDoubleVoting, msg.DuplicateVoteEvidence.String()), sdk.NewAttribute(ccvtypes.AttributeChainID, msg.InfractionBlockHeader.Header.ChainID), sdk.NewAttribute(ccvtypes.AttributeSubmitterAddress, msg.Submitter), diff --git a/x/ccv/provider/types/msg.go b/x/ccv/provider/types/msg.go index b6741a0a43..e5df03e335 100644 --- a/x/ccv/provider/types/msg.go +++ b/x/ccv/provider/types/msg.go @@ -22,6 +22,7 @@ const ( var ( _ sdk.Msg = &MsgAssignConsumerKey{} _ sdk.Msg = &MsgSubmitConsumerMisbehaviour{} + _ sdk.Msg = &MsgSubmitConsumerDoubleVoting{} ) // NewMsgAssignConsumerKey creates a new MsgAssignConsumerKey instance. @@ -222,6 +223,10 @@ func (msg MsgSubmitConsumerDoubleVoting) ValidateBasic() error { return fmt.Errorf("invalid signed header in infraction block header, 'SignedHeader.Header' is nil") } + if msg.InfractionBlockHeader.ValidatorSet == nil { + return fmt.Errorf("invalid infraction block header, validator set is nil") + } + return nil } diff --git a/x/ccv/types/errors.go b/x/ccv/types/errors.go index e0cb663219..4fbb65398a 100644 --- a/x/ccv/types/errors.go +++ b/x/ccv/types/errors.go @@ -6,24 +6,24 @@ import ( // CCV sentinel errors var ( - ErrInvalidPacketData = errorsmod.Register(ModuleName, 2, "invalid CCV packet data") - ErrInvalidPacketTimeout = errorsmod.Register(ModuleName, 3, "invalid packet timeout") - ErrInvalidVersion = errorsmod.Register(ModuleName, 4, "invalid CCV version") - ErrInvalidChannelFlow = errorsmod.Register(ModuleName, 5, "invalid message sent to channel end") - ErrInvalidConsumerChain = errorsmod.Register(ModuleName, 6, "invalid consumer chain") - ErrInvalidProviderChain = errorsmod.Register(ModuleName, 7, "invalid provider chain") - ErrInvalidStatus = errorsmod.Register(ModuleName, 8, "invalid channel status") - ErrInvalidGenesis = errorsmod.Register(ModuleName, 9, "invalid genesis state") - ErrDuplicateChannel = errorsmod.Register(ModuleName, 10, "CCV channel already exists") - ErrInvalidVSCMaturedId = errorsmod.Register(ModuleName, 11, "invalid vscId for VSC packet") - ErrInvalidVSCMaturedTime = errorsmod.Register(ModuleName, 12, "invalid maturity time for VSC packet") - ErrInvalidConsumerState = errorsmod.Register(ModuleName, 13, "provider chain has invalid state for consumer chain") - ErrInvalidConsumerClient = errorsmod.Register(ModuleName, 14, "ccv channel is not built on correct client") - ErrInvalidProposal = errorsmod.Register(ModuleName, 15, "invalid proposal") - ErrInvalidHandshakeMetadata = errorsmod.Register(ModuleName, 16, "invalid provider handshake metadata") - ErrChannelNotFound = errorsmod.Register(ModuleName, 17, "channel not found") - ErrClientNotFound = errorsmod.Register(ModuleName, 18, "client not found") - ErrDuplicateConsumerChain = errorsmod.Register(ModuleName, 19, "consumer chain already exists") - ErrConsumerChainNotFound = errorsmod.Register(ModuleName, 20, "consumer chain not found") - ErrInvalidEvidence = errorsmod.Register(ModuleName, 21, "invalid consumer double voting evidence") + ErrInvalidPacketData = errorsmod.Register(ModuleName, 2, "invalid CCV packet data") + ErrInvalidPacketTimeout = errorsmod.Register(ModuleName, 3, "invalid packet timeout") + ErrInvalidVersion = errorsmod.Register(ModuleName, 4, "invalid CCV version") + ErrInvalidChannelFlow = errorsmod.Register(ModuleName, 5, "invalid message sent to channel end") + ErrInvalidConsumerChain = errorsmod.Register(ModuleName, 6, "invalid consumer chain") + ErrInvalidProviderChain = errorsmod.Register(ModuleName, 7, "invalid provider chain") + ErrInvalidStatus = errorsmod.Register(ModuleName, 8, "invalid channel status") + ErrInvalidGenesis = errorsmod.Register(ModuleName, 9, "invalid genesis state") + ErrDuplicateChannel = errorsmod.Register(ModuleName, 10, "CCV channel already exists") + ErrInvalidVSCMaturedId = errorsmod.Register(ModuleName, 11, "invalid vscId for VSC packet") + ErrInvalidVSCMaturedTime = errorsmod.Register(ModuleName, 12, "invalid maturity time for VSC packet") + ErrInvalidConsumerState = errorsmod.Register(ModuleName, 13, "provider chain has invalid state for consumer chain") + ErrInvalidConsumerClient = errorsmod.Register(ModuleName, 14, "ccv channel is not built on correct client") + ErrInvalidProposal = errorsmod.Register(ModuleName, 15, "invalid proposal") + ErrInvalidHandshakeMetadata = errorsmod.Register(ModuleName, 16, "invalid provider handshake metadata") + ErrChannelNotFound = errorsmod.Register(ModuleName, 17, "channel not found") + ErrClientNotFound = errorsmod.Register(ModuleName, 18, "client not found") + ErrDuplicateConsumerChain = errorsmod.Register(ModuleName, 19, "consumer chain already exists") + ErrConsumerChainNotFound = errorsmod.Register(ModuleName, 20, "consumer chain not found") + ErrInvalidDoubleVotingEvidence = errorsmod.Register(ModuleName, 21, "invalid consumer double voting evidence") ) diff --git a/x/ccv/types/events.go b/x/ccv/types/events.go index 28796144fe..6750bde169 100644 --- a/x/ccv/types/events.go +++ b/x/ccv/types/events.go @@ -10,6 +10,7 @@ const ( EventTypeAssignConsumerKey = "assign_consumer_key" EventTypeRegisterConsumerRewardDenom = "register_consumer_reward_denom" EventTypeSubmitConsumerMisbehaviour = "submit_consumer_misbehaviour" + EventTypeSubmitConsumerDoubleVoting = "submit_consumer_double_voting" EventTypeExecuteConsumerChainSlash = "execute_consumer_chain_slash" EventTypeFeeDistribution = "fee_distribution" EventTypeConsumerSlashRequest = "consumer_slash_request" From 88e07170e071b27d1d4522da6392546bf0e90f63 Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Thu, 14 Sep 2023 14:59:04 +0200 Subject: [PATCH 12/12] update changelog and fix nits --- CHANGELOG.md | 7 +++++++ docs/docs/features/slashing.md | 16 ++++++++++++++++ tests/e2e/actions.go | 2 +- x/ccv/provider/client/proposal_handler.go | 2 +- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe47bfd21..a5e309af94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ Add an entry to the unreleased section whenever merging a PR to main that is not ## v2.1.0-lsm-provider * (feature!) [#1280](https://github.com/cosmos/interchain-security/pull/1280) provider proposal for changing reward denoms +* (feature!) [#826](https://github.com/cosmos/interchain-security/pull/826) add new endpoint to provider to handle consumer light client attacks +* (feature!) [#1227](https://github.com/cosmos/interchain-security/pull/1227) add new endpoint to provider to handle consumer double signing attacks + + +### Cryptographic verification of equivocation +* New feature enabling the provider chain to verify equivocation evidence on its own instead of trusting consumer chains, see [EPIC](https://github.com/cosmos/interchain-security/issues/732). + ## v2.0.0-lsm diff --git a/docs/docs/features/slashing.md b/docs/docs/features/slashing.md index a28b16e8c2..e1a51bf8b2 100644 --- a/docs/docs/features/slashing.md +++ b/docs/docs/features/slashing.md @@ -34,3 +34,19 @@ The offending validator will effectively get slashed and tombstoned on all consu You can find instructions on creating `EquivocationProposal`s [here](./proposals#equivocationproposal). + +# Cryptographic verification of equivocation +The Cryptographic verification of equivocation allows external agents to submit evidences of light client and double signing attack observed on a consumer chain. When a valid evidence is received, the malicious validators will be permanently jailed on the provider. + +The feature is outlined in this [ADR-005](../adrs/adr-005-cryptographic-equivocation-verification.md) + +By sending a `MsgSubmitConsumerMisbehaviour` or a `MsgSubmitConsumerDoubleVoting` transaction, the provider will + verify the reported equivocation and, if successful, jail the malicious validator. + +:::info +Note that this feature can only lead to the jailing of the validators responsible for an attack on a consumer chain. However, an [equivocation proposal](#double-signing-equivocation) can still be submitted to execute the slashing and the tombstoning of the a malicious validator afterwards. +::: + + + + diff --git a/tests/e2e/actions.go b/tests/e2e/actions.go index 2185cc963b..4419d1dcff 100644 --- a/tests/e2e/actions.go +++ b/tests/e2e/actions.go @@ -1729,7 +1729,7 @@ func (tr TestRun) submitChangeRewardDenomsProposal(action submitChangeRewardDeno //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. // CHANGE REWARDS DENOM PROPOSAL bz, err = exec.Command("docker", "exec", tr.containerConfig.instanceName, providerChain.binaryName, - "tx", "gov", "submit-legacy-proposal", "change-reward-denoms", "/change-reward-denoms-proposal.json", + "tx", "gov", "submit-proposal", "change-reward-denoms", "/change-reward-denoms-proposal.json", `--from`, `validator`+fmt.Sprint(action.from), `--chain-id`, string(providerChain.chainId), `--home`, tr.getValidatorHome(providerChain.chainId, action.from), diff --git a/x/ccv/provider/client/proposal_handler.go b/x/ccv/provider/client/proposal_handler.go index dd33b83f15..499f4e3b34 100644 --- a/x/ccv/provider/client/proposal_handler.go +++ b/x/ccv/provider/client/proposal_handler.go @@ -227,7 +227,7 @@ func SubmitChangeRewardDenomsProposalTxCmd() *cobra.Command { The proposal details must be supplied via a JSON file. Example: - $ tx gov submit-legacy-proposal change-reward-denoms --from= + $ tx gov submit-proposal change-reward-denoms --from= Where proposal.json contains: {