Skip to content

Commit

Permalink
feat: add handler for consumer double voting (#1232)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sainoe committed Aug 28, 2023
1 parent 292ad75 commit f168b9b
Show file tree
Hide file tree
Showing 18 changed files with 1,417 additions and 65 deletions.
19 changes: 19 additions & 0 deletions proto/interchain_security/ccv/provider/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {}
123 changes: 123 additions & 0 deletions tests/integration/double_vote.go
Original file line number Diff line number Diff line change
@@ -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()))
}
})
}
}
3 changes: 1 addition & 2 deletions tests/integration/misbehaviour.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
}

Expand Down
56 changes: 56 additions & 0 deletions testutil/crypto/evidence.go
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 9 additions & 1 deletion testutil/integration/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ func TestRecycleTransferChannel(t *testing.T) {
}

//
// Misbehaviour test
// Misbehaviour tests
//

func TestHandleConsumerMisbehaviour(t *testing.T) {
Expand All @@ -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")
}
38 changes: 38 additions & 0 deletions third_party/proto/tendermint/types/evidence.proto
Original file line number Diff line number Diff line change
@@ -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];
}
46 changes: 46 additions & 0 deletions x/ccv/provider/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,6 +30,7 @@ func GetTxCmd() *cobra.Command {
cmd.AddCommand(NewAssignConsumerKeyCmd())
cmd.AddCommand(NewRegisterConsumerRewardDenomCmd())
cmd.AddCommand(NewSubmitConsumerMisbehaviourCmd())
cmd.AddCommand(NewSubmitConsumerDoubleVotingCmd())

return cmd
}
Expand Down Expand Up @@ -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
}
3 changes: 3 additions & 0 deletions x/ccv/provider/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading

0 comments on commit f168b9b

Please sign in to comment.