diff --git a/tests/integration/actions.go b/tests/integration/actions.go index a23bdc76d1..22b365557e 100644 --- a/tests/integration/actions.go +++ b/tests/integration/actions.go @@ -11,10 +11,12 @@ import ( "sync" "time" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" clienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" consumertypes "github.com/cosmos/interchain-security/x/ccv/consumer/types" "github.com/cosmos/interchain-security/x/ccv/provider/client" + "github.com/cosmos/interchain-security/x/ccv/provider/types" "github.com/tidwall/gjson" ) @@ -398,6 +400,75 @@ func (tr TestRun) submitParamChangeProposal( } } +type submitEquivocationProposalAction struct { + chain chainID + height int64 + time time.Time + power int64 + validator validatorID + deposit uint + from validatorID +} + +func (tr TestRun) submitEquivocationProposal(action submitEquivocationProposalAction, verbose bool) { + val := tr.validatorConfigs[action.validator] + providerChain := tr.chainConfigs[chainID("provi")] + + prop := client.EquivocationProposalJSON{ + EquivocationProposal: types.EquivocationProposal{ + Title: "Validator equivocation!", + Description: fmt.Sprintf("Validator: %s has committed an equivocation infraction on chainID: %s", action.validator, action.chain), + Equivocations: []*evidencetypes.Equivocation{ + { + Height: action.height, + Time: action.time, + Power: action.power, + ConsensusAddress: val.valconsAddress, + }, + }, + }, + Deposit: fmt.Sprint(action.deposit) + `stake`, + } + + bz, err := json.Marshal(prop) + if err != nil { + log.Fatal(err) + } + + jsonStr := string(bz) + if strings.Contains(jsonStr, "'") { + log.Fatal("prop json contains single quote") + } + + //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. + bz, err = exec.Command("docker", "exec", tr.containerConfig.instanceName, + "/bin/bash", "-c", fmt.Sprintf(`echo '%s' > %s`, jsonStr, "/equivocation-proposal.json")).CombinedOutput() + + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. + bz, err = exec.Command("docker", "exec", tr.containerConfig.instanceName, providerChain.binaryName, + + "tx", "gov", "submit-proposal", "equivocation", + "/equivocation-proposal.json", + + `--from`, `validator`+fmt.Sprint(action.from), + `--chain-id`, string(providerChain.chainId), + `--home`, tr.getValidatorHome(providerChain.chainId, action.from), + `--node`, tr.getValidatorNode(providerChain.chainId, action.from), + `--gas`, "9000000", + `--keyring-backend`, `test`, + `-b`, `block`, + `-y`, + ).CombinedOutput() + + if err != nil { + log.Fatal(err, "\n", string(bz)) + } +} + type voteGovProposalAction struct { chain chainID from []validatorID diff --git a/tests/integration/config.go b/tests/integration/config.go index 0df21b26eb..67331b25ab 100644 --- a/tests/integration/config.go +++ b/tests/integration/config.go @@ -302,6 +302,47 @@ func MultiConsumerTestRun() TestRun { } } +func EquivocationProposalTestRun() TestRun { + return TestRun{ + name: "equivocation", + containerConfig: ContainerConfig{ + containerName: "interchain-security-equiv-container", + instanceName: "interchain-security-equiv-instance", + ccvVersion: "1", + now: time.Now(), + }, + validatorConfigs: getDefaultValidators(), + 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 = \"2\" | " + + ".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\"", + }, + }, + } +} + func (s *TestRun) SetLocalSDKPath(path string) { if path != "" { fmt.Println("USING LOCAL SDK", path) diff --git a/tests/integration/main.go b/tests/integration/main.go index e28885f31b..155f8b13a1 100644 --- a/tests/integration/main.go +++ b/tests/integration/main.go @@ -37,6 +37,7 @@ func main() { {DefaultTestRun(), happyPathSteps}, {DemocracyTestRun(), democracySteps}, {SlashThrottleTestRun(), slashThrottleSteps}, + {EquivocationProposalTestRun(), equivocationProposalSteps}, } if includeMultiConsumer != nil && *includeMultiConsumer { testRuns = append(testRuns, testRunWithSteps{MultiConsumerTestRun(), multipleConsumers}) @@ -93,6 +94,8 @@ func (tr *TestRun) runStep(step Step, verbose bool) { tr.submitConsumerAdditionProposal(action, verbose) case submitConsumerRemovalProposalAction: tr.submitConsumerRemovalProposal(action, verbose) + case submitEquivocationProposalAction: + tr.submitEquivocationProposal(action, verbose) case submitParamChangeProposalAction: tr.submitParamChangeProposal(action, verbose) case voteGovProposalAction: diff --git a/tests/integration/state.go b/tests/integration/state.go index 2b3fe95fdb..ae35767f05 100644 --- a/tests/integration/state.go +++ b/tests/integration/state.go @@ -60,6 +60,16 @@ type ConsumerRemovalProposal struct { func (p ConsumerRemovalProposal) isProposal() {} +type EquivocationProposal struct { + Height uint + Power uint + ConsensusAddress string + Deposit uint + Status string +} + +func (p EquivocationProposal) isProposal() {} + type Rewards struct { IsRewarded map[validatorID]bool //if true it will calculate if the validator/delegator is rewarded between 2 successive blocks, @@ -397,6 +407,15 @@ func (tr TestRun) getProposal(chain chainID, proposal uint) Proposal { StopTime: int(stopTime.Milliseconds()), } + case "/interchain_security.ccv.provider.v1.EquivocationProposal": + return EquivocationProposal{ + Deposit: uint(deposit), + Status: status, + Height: uint(gjson.Get(string(bz), `content.equivocations.0.height`).Uint()), + Power: uint(gjson.Get(string(bz), `content.equivocations.0.power`).Uint()), + ConsensusAddress: gjson.Get(string(bz), `content.equivocations.0.consensus_address`).String(), + } + case "/cosmos.params.v1beta1.ParameterChangeProposal": return ParamsProposal{ Deposit: uint(deposit), diff --git a/tests/integration/steps.go b/tests/integration/steps.go index e0250e3216..e680c2cd45 100644 --- a/tests/integration/steps.go +++ b/tests/integration/steps.go @@ -38,7 +38,6 @@ var democracySteps = concatSteps( stepsDemocracy("democ"), ) -//nolint var multipleConsumers = concatSteps( stepsStartChains([]string{"consu", "densu"}, false), stepsMultiConsumerDelegate("consu", "densu"), @@ -48,3 +47,9 @@ var multipleConsumers = concatSteps( stepsMultiConsumerDowntimeFromProvider("consu", "densu"), stepsDoubleSign("consu", "densu"), // double sign on one of the chains ) + +var equivocationProposalSteps = concatSteps( + stepsStartChains([]string{"consu"}, false), + stepsDelegate("consu"), + stepsSubmitEquivocationProposal("consu"), +) diff --git a/tests/integration/steps_submit_equivocation_proposal.go b/tests/integration/steps_submit_equivocation_proposal.go new file mode 100644 index 0000000000..b2c1dc194e --- /dev/null +++ b/tests/integration/steps_submit_equivocation_proposal.go @@ -0,0 +1,108 @@ +package main + +import "time" + +// submits an equivocation proposal, votes on it, and tomstones the equivocating validator +func stepsSubmitEquivocationProposal(consumerName string) []Step { + s := []Step{ + { + // bob submits a proposal to slash himself + action: submitEquivocationProposalAction{ + chain: chainID("consu"), + from: validatorID("bob"), + deposit: 10000001, + height: 10, + time: time.Now(), // not sure what time in equivocations means + power: 500, + validator: validatorID("bob"), + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 500, + validatorID("carol"): 500, + }, + ValBalances: &map[validatorID]uint{ + validatorID("bob"): 9489999999, + }, + Proposals: &map[uint]Proposal{ + 2: EquivocationProposal{ + Deposit: 10000001, + Status: "PROPOSAL_STATUS_VOTING_PERIOD", + ConsensusAddress: "cosmosvalcons1nx7n5uh0ztxsynn4sje6eyq2ud6rc6klc96w39", + Power: 500, + Height: 10, + }, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 500, + validatorID("carol"): 500, + }, + }, + }, + }, + { + action: voteGovProposalAction{ + chain: chainID("provi"), + from: []validatorID{validatorID("alice"), validatorID("bob"), validatorID("carol")}, + vote: []string{"yes", "yes", "yes"}, + propNumber: 2, + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 0, // bob is slashed after proposal passes + validatorID("carol"): 500, + }, + Proposals: &map[uint]Proposal{ + 2: EquivocationProposal{ + Deposit: 10000001, + Status: "PROPOSAL_STATUS_PASSED", + ConsensusAddress: "cosmosvalcons1nx7n5uh0ztxsynn4sje6eyq2ud6rc6klc96w39", + Power: 500, + Height: 10, + }, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 500, // slash not reflected in consumer chain + validatorID("carol"): 500, + }, + }, + }, + }, + { + // relay power change to consumer1 + action: relayPacketsAction{ + chain: chainID("provi"), + port: "provider", + channel: 0, + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 0, + validatorID("carol"): 500, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 511, + validatorID("bob"): 0, // slash relayed to consumer chain + validatorID("carol"): 500, + }, + }, + }, + }, + } + + return s +}