From 93a0db5a2f760f101f2dac77ca94fb52b5054ef4 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Thu, 7 Sep 2023 17:38:02 +0200 Subject: [PATCH 01/29] first version --- x/ccv/provider/keeper/double_vote.go | 39 ++++++++++++++++++- x/ccv/provider/keeper/misbehaviour.go | 3 +- x/ccv/provider/keeper/punish_validator.go | 7 ++-- .../provider/keeper/punish_validator_test.go | 2 +- x/ccv/types/expected_keepers.go | 2 + 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go index ee5f716ef8..749c964902 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -3,6 +3,7 @@ package keeper import ( "bytes" "fmt" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" @@ -13,6 +14,41 @@ import ( tmtypes "github.com/tendermint/tendermint/types" ) +func (k Keeper) slashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) { + val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) + + if !found { + //logger.Error("validator not found or is unbonded", providerAddr.String()) + return + } + valOperatorAddress := val.GetOperator() + + undelegationsInTokens := sdk.NewInt(0) + for _, v := range k.stakingKeeper.GetUnbondingDelegationsFromValidator(ctx, valOperatorAddress) { + for _, entry := range v.Entries { + undelegationsInTokens = undelegationsInTokens.Add(entry.InitialBalance) + } + } + + redelegationsInTokens := sdk.NewInt(0) + for _, v := range k.stakingKeeper.GetRedelegationsFromSrcValidator(ctx, valOperatorAddress) { + for _, entry := range v.Entries { + redelegationsInTokens = redelegationsInTokens.Add(entry.InitialBalance) + } + } + + powerReduction := k.stakingKeeper.PowerReduction(ctx) + undelegationsAndRedelegationsInPower := sdk.TokensToConsensusPower( + undelegationsInTokens.Add(redelegationsInTokens), powerReduction) + + power := k.stakingKeeper.GetLastValidatorPower(ctx, valOperatorAddress) + totalPower := power + undelegationsAndRedelegationsInPower + slashFraction := k.slashingKeeper.SlashFractionDoubleSign(ctx) + + // TODO: what if it's another key ???? + k.stakingKeeper.Slash(ctx, providerAddr.ToSdkConsAddr(), 0, totalPower, slashFraction, stakingtypes.DoubleSign) +} + // 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( @@ -34,7 +70,8 @@ func (k Keeper) HandleConsumerDoubleVoting( ) // execute the jailing - k.JailValidator(ctx, providerAddr) + k.slashValidator(ctx, providerAddr) + k.JailAndTombstoneValidator(ctx, providerAddr) k.Logger(ctx).Info( "confirmed equivocation", diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 35d9219324..40ae2c6f11 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -41,7 +41,8 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty misbehaviour.Header1.Header.ChainID, types.NewConsumerConsAddress(sdk.ConsAddress(v.Address.Bytes())), ) - k.JailValidator(ctx, providerAddr) + k.slashValidator(ctx, providerAddr) + k.JailAndTombstoneValidator(ctx, providerAddr) provAddrs = append(provAddrs, providerAddr) } diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index f4648cc641..9323128a60 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -6,11 +6,11 @@ import ( "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" ) -// JailValidator jails the validator with the given provider consensus address +// JailAndTombstoneValidator 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) { +func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) { logger := k.Logger(ctx) // get validator @@ -34,5 +34,6 @@ func (k Keeper) JailValidator(ctx sdk.Context, providerAddr types.ProviderConsAd // 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 + // TODO: do we need to jail if we tombstone, that's what cosmos-sdk does + k.slashingKeeper.Tombstone(ctx, providerAddr.ToSdkConsAddr()) } diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index 50da9ae4bb..286da69fe2 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -125,7 +125,7 @@ func TestJailValidator(t *testing.T) { gomock.InOrder(tc.expectedCalls(ctx, mocks, tc.provAddr)...) // Execute method and assert expected mock calls - providerKeeper.JailValidator(ctx, tc.provAddr) + providerKeeper.JailAndTombstoneValidator(ctx, tc.provAddr) ctrl.Finish() } diff --git a/x/ccv/types/expected_keepers.go b/x/ccv/types/expected_keepers.go index b4dea9b670..7cdf607057 100644 --- a/x/ccv/types/expected_keepers.go +++ b/x/ccv/types/expected_keepers.go @@ -25,6 +25,8 @@ type StakingKeeper interface { GetValidatorUpdates(ctx sdk.Context) []abci.ValidatorUpdate UnbondingCanComplete(ctx sdk.Context, id uint64) error UnbondingTime(ctx sdk.Context) time.Duration + GetUnbondingDelegationsFromValidator(ctx sdk.Context, valAddr sdk.ValAddress) (ubds []stakingtypes.UnbondingDelegation) + GetRedelegationsFromSrcValidator(ctx sdk.Context, valAddr sdk.ValAddress) (reds []stakingtypes.Redelegation) GetValidatorByConsAddr(ctx sdk.Context, consAddr sdk.ConsAddress) (validator stakingtypes.Validator, found bool) GetLastValidatorPower(ctx sdk.Context, operator sdk.ValAddress) (power int64) // slash the validator and delegators of the validator, specifying offence height, offence power, and slash fraction From ea87bdc97d13b38a261a8e6cdaa2119e7cc0cb71 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Fri, 8 Sep 2023 10:44:46 +0200 Subject: [PATCH 02/29] fix mocks --- testutil/keeper/mocks.go | 52 +++++++++++++++++++++++++-------- x/ccv/types/expected_keepers.go | 4 +-- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/testutil/keeper/mocks.go b/testutil/keeper/mocks.go index 1e792e3a4a..b75d1ad145 100644 --- a/testutil/keeper/mocks.go +++ b/testutil/keeper/mocks.go @@ -116,6 +116,34 @@ func (mr *MockStakingKeeperMockRecorder) GetLastValidators(ctx interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastValidators", reflect.TypeOf((*MockStakingKeeper)(nil).GetLastValidators), ctx) } +// GetRedelegationsFromSrcValidator mocks base method. +func (m *MockStakingKeeper) GetRedelegationsFromSrcValidator(ctx types.Context, valAddr types.ValAddress) []types4.Redelegation { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRedelegationsFromSrcValidator", ctx, valAddr) + ret0, _ := ret[0].([]types4.Redelegation) + return ret0 +} + +// GetRedelegationsFromSrcValidator indicates an expected call of GetRedelegationsFromSrcValidator. +func (mr *MockStakingKeeperMockRecorder) GetRedelegationsFromSrcValidator(ctx, valAddr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRedelegationsFromSrcValidator", reflect.TypeOf((*MockStakingKeeper)(nil).GetRedelegationsFromSrcValidator), ctx, valAddr) +} + +// GetUnbondingDelegationsFromValidator mocks base method. +func (m *MockStakingKeeper) GetUnbondingDelegationsFromValidator(ctx types.Context, valAddr types.ValAddress) []types4.UnbondingDelegation { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUnbondingDelegationsFromValidator", ctx, valAddr) + ret0, _ := ret[0].([]types4.UnbondingDelegation) + return ret0 +} + +// GetUnbondingDelegationsFromValidator indicates an expected call of GetUnbondingDelegationsFromValidator. +func (mr *MockStakingKeeperMockRecorder) GetUnbondingDelegationsFromValidator(ctx, valAddr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnbondingDelegationsFromValidator", reflect.TypeOf((*MockStakingKeeper)(nil).GetUnbondingDelegationsFromValidator), ctx, valAddr) +} + // GetValidator mocks base method. func (m *MockStakingKeeper) GetValidator(ctx types.Context, addr types.ValAddress) (types4.Validator, bool) { m.ctrl.T.Helper() @@ -781,6 +809,18 @@ func (mr *MockClientKeeperMockRecorder) GetSelfConsensusState(ctx, height interf return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSelfConsensusState", reflect.TypeOf((*MockClientKeeper)(nil).GetSelfConsensusState), ctx, height) } +// 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) +} + // MockDistributionKeeper is a mock of DistributionKeeper interface. type MockDistributionKeeper struct { ctrl *gomock.Controller @@ -818,18 +858,6 @@ 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/types/expected_keepers.go b/x/ccv/types/expected_keepers.go index 7cdf607057..bc55ebbf7e 100644 --- a/x/ccv/types/expected_keepers.go +++ b/x/ccv/types/expected_keepers.go @@ -25,8 +25,6 @@ type StakingKeeper interface { GetValidatorUpdates(ctx sdk.Context) []abci.ValidatorUpdate UnbondingCanComplete(ctx sdk.Context, id uint64) error UnbondingTime(ctx sdk.Context) time.Duration - GetUnbondingDelegationsFromValidator(ctx sdk.Context, valAddr sdk.ValAddress) (ubds []stakingtypes.UnbondingDelegation) - GetRedelegationsFromSrcValidator(ctx sdk.Context, valAddr sdk.ValAddress) (reds []stakingtypes.Redelegation) GetValidatorByConsAddr(ctx sdk.Context, consAddr sdk.ConsAddress) (validator stakingtypes.Validator, found bool) GetLastValidatorPower(ctx sdk.Context, operator sdk.ValAddress) (power int64) // slash the validator and delegators of the validator, specifying offence height, offence power, and slash fraction @@ -46,6 +44,8 @@ type StakingKeeper interface { GetLastTotalPower(ctx sdk.Context) sdk.Int GetLastValidators(ctx sdk.Context) (validators []stakingtypes.Validator) BondDenom(ctx sdk.Context) (res string) + GetUnbondingDelegationsFromValidator(ctx sdk.Context, valAddr sdk.ValAddress) (ubds []stakingtypes.UnbondingDelegation) + GetRedelegationsFromSrcValidator(ctx sdk.Context, valAddr sdk.ValAddress) (reds []stakingtypes.Redelegation) } type EvidenceKeeper interface { From 177e1dbe0bc4e76dce9978c1f27ce16992532228 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Fri, 8 Sep 2023 13:59:27 +0200 Subject: [PATCH 03/29] move slashing to other file and check tombstoning --- tests/integration/double_vote.go | 3 +- tests/integration/misbehaviour.go | 1 + testutil/keeper/mocks.go | 41 +++++++++----- x/ccv/provider/keeper/double_vote.go | 39 +------------ x/ccv/provider/keeper/misbehaviour.go | 2 +- x/ccv/provider/keeper/punish_validator.go | 67 ++++++++++++++++++++--- x/ccv/types/expected_keepers.go | 2 + 7 files changed, 93 insertions(+), 62 deletions(-) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index c79b92115e..2eca555678 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -185,7 +185,8 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { if tc.expPass { s.Require().NoError(err) - // verifies that the jailing has occurred + // verifies that the tombstoning and jailing has occurred + s.Require().True(s.providerApp.GetTestSlashingKeeper().IsTombstoned(provCtx, provAddr.ToSdkConsAddr())) s.Require().True(s.providerApp.GetTestStakingKeeper().IsValidatorJailed(provCtx, provAddr.ToSdkConsAddr())) } else { s.Require().Error(err) diff --git a/tests/integration/misbehaviour.go b/tests/integration/misbehaviour.go index f7cebc84c0..66e1388474 100644 --- a/tests/integration/misbehaviour.go +++ b/tests/integration/misbehaviour.go @@ -63,6 +63,7 @@ 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.ToSdkConsAddr())) } } diff --git a/testutil/keeper/mocks.go b/testutil/keeper/mocks.go index b75d1ad145..848d26a5fe 100644 --- a/testutil/keeper/mocks.go +++ b/testutil/keeper/mocks.go @@ -12,13 +12,14 @@ import ( types "github.com/cosmos/cosmos-sdk/types" types0 "github.com/cosmos/cosmos-sdk/x/auth/types" types1 "github.com/cosmos/cosmos-sdk/x/capability/types" + exported "github.com/cosmos/cosmos-sdk/x/evidence/exported" types2 "github.com/cosmos/cosmos-sdk/x/evidence/types" types3 "github.com/cosmos/cosmos-sdk/x/slashing/types" types4 "github.com/cosmos/cosmos-sdk/x/staking/types" types5 "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" types6 "github.com/cosmos/ibc-go/v4/modules/core/03-connection/types" types7 "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" - exported "github.com/cosmos/ibc-go/v4/modules/core/exported" + exported0 "github.com/cosmos/ibc-go/v4/modules/core/exported" gomock "github.com/golang/mock/gomock" types8 "github.com/tendermint/tendermint/abci/types" ) @@ -395,6 +396,18 @@ func (mr *MockEvidenceKeeperMockRecorder) HandleEquivocationEvidence(ctx, eviden return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleEquivocationEvidence", reflect.TypeOf((*MockEvidenceKeeper)(nil).HandleEquivocationEvidence), ctx, evidence) } +// SetEvidence mocks base method. +func (m *MockEvidenceKeeper) SetEvidence(ctx types.Context, evidence exported.Evidence) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetEvidence", ctx, evidence) +} + +// SetEvidence indicates an expected call of SetEvidence. +func (mr *MockEvidenceKeeperMockRecorder) SetEvidence(ctx, evidence interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEvidence", reflect.TypeOf((*MockEvidenceKeeper)(nil).SetEvidence), ctx, evidence) +} + // MockSlashingKeeper is a mock of SlashingKeeper interface. type MockSlashingKeeper struct { ctrl *gomock.Controller @@ -581,7 +594,7 @@ func (mr *MockChannelKeeperMockRecorder) GetNextSequenceSend(ctx, portID, channe } // SendPacket mocks base method. -func (m *MockChannelKeeper) SendPacket(ctx types.Context, channelCap *types1.Capability, packet exported.PacketI) error { +func (m *MockChannelKeeper) SendPacket(ctx types.Context, channelCap *types1.Capability, packet exported0.PacketI) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SendPacket", ctx, channelCap, packet) ret0, _ := ret[0].(error) @@ -595,7 +608,7 @@ func (mr *MockChannelKeeperMockRecorder) SendPacket(ctx, channelCap, packet inte } // WriteAcknowledgement mocks base method. -func (m *MockChannelKeeper) WriteAcknowledgement(ctx types.Context, chanCap *types1.Capability, packet exported.PacketI, acknowledgement exported.Acknowledgement) error { +func (m *MockChannelKeeper) WriteAcknowledgement(ctx types.Context, chanCap *types1.Capability, packet exported0.PacketI, acknowledgement exported0.Acknowledgement) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "WriteAcknowledgement", ctx, chanCap, packet, acknowledgement) ret0, _ := ret[0].(error) @@ -707,7 +720,7 @@ func (m *MockClientKeeper) EXPECT() *MockClientKeeperMockRecorder { } // CheckMisbehaviourAndUpdateState mocks base method. -func (m *MockClientKeeper) CheckMisbehaviourAndUpdateState(ctx types.Context, misbehaviour exported.Misbehaviour) error { +func (m *MockClientKeeper) CheckMisbehaviourAndUpdateState(ctx types.Context, misbehaviour exported0.Misbehaviour) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CheckMisbehaviourAndUpdateState", ctx, misbehaviour) ret0, _ := ret[0].(error) @@ -735,7 +748,7 @@ func (mr *MockClientKeeperMockRecorder) ClientStore(ctx, clientID interface{}) * } // CreateClient mocks base method. -func (m *MockClientKeeper) CreateClient(ctx types.Context, clientState exported.ClientState, consensusState exported.ConsensusState) (string, error) { +func (m *MockClientKeeper) CreateClient(ctx types.Context, clientState exported0.ClientState, consensusState exported0.ConsensusState) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateClient", ctx, clientState, consensusState) ret0, _ := ret[0].(string) @@ -750,10 +763,10 @@ func (mr *MockClientKeeperMockRecorder) CreateClient(ctx, clientState, consensus } // GetClientConsensusState mocks base method. -func (m *MockClientKeeper) GetClientConsensusState(ctx types.Context, clientID string, height exported.Height) (exported.ConsensusState, bool) { +func (m *MockClientKeeper) GetClientConsensusState(ctx types.Context, clientID string, height exported0.Height) (exported0.ConsensusState, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetClientConsensusState", ctx, clientID, height) - ret0, _ := ret[0].(exported.ConsensusState) + ret0, _ := ret[0].(exported0.ConsensusState) ret1, _ := ret[1].(bool) return ret0, ret1 } @@ -765,10 +778,10 @@ func (mr *MockClientKeeperMockRecorder) GetClientConsensusState(ctx, clientID, h } // GetClientState mocks base method. -func (m *MockClientKeeper) GetClientState(ctx types.Context, clientID string) (exported.ClientState, bool) { +func (m *MockClientKeeper) GetClientState(ctx types.Context, clientID string) (exported0.ClientState, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetClientState", ctx, clientID) - ret0, _ := ret[0].(exported.ClientState) + ret0, _ := ret[0].(exported0.ClientState) ret1, _ := ret[1].(bool) return ret0, ret1 } @@ -780,10 +793,10 @@ func (mr *MockClientKeeperMockRecorder) GetClientState(ctx, clientID interface{} } // GetLatestClientConsensusState mocks base method. -func (m *MockClientKeeper) GetLatestClientConsensusState(ctx types.Context, clientID string) (exported.ConsensusState, bool) { +func (m *MockClientKeeper) GetLatestClientConsensusState(ctx types.Context, clientID string) (exported0.ConsensusState, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetLatestClientConsensusState", ctx, clientID) - ret0, _ := ret[0].(exported.ConsensusState) + ret0, _ := ret[0].(exported0.ConsensusState) ret1, _ := ret[1].(bool) return ret0, ret1 } @@ -795,10 +808,10 @@ func (mr *MockClientKeeperMockRecorder) GetLatestClientConsensusState(ctx, clien } // GetSelfConsensusState mocks base method. -func (m *MockClientKeeper) GetSelfConsensusState(ctx types.Context, height exported.Height) (exported.ConsensusState, error) { +func (m *MockClientKeeper) GetSelfConsensusState(ctx types.Context, height exported0.Height) (exported0.ConsensusState, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSelfConsensusState", ctx, height) - ret0, _ := ret[0].(exported.ConsensusState) + ret0, _ := ret[0].(exported0.ConsensusState) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -810,7 +823,7 @@ func (mr *MockClientKeeperMockRecorder) GetSelfConsensusState(ctx, height interf } // SetClientState mocks base method. -func (m *MockClientKeeper) SetClientState(ctx types.Context, clientID string, clientState exported.ClientState) { +func (m *MockClientKeeper) SetClientState(ctx types.Context, clientID string, clientState exported0.ClientState) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetClientState", ctx, clientID, clientState) } diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go index 749c964902..d4f13fe671 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -3,8 +3,6 @@ package keeper import ( "bytes" "fmt" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -14,41 +12,6 @@ import ( tmtypes "github.com/tendermint/tendermint/types" ) -func (k Keeper) slashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) { - val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) - - if !found { - //logger.Error("validator not found or is unbonded", providerAddr.String()) - return - } - valOperatorAddress := val.GetOperator() - - undelegationsInTokens := sdk.NewInt(0) - for _, v := range k.stakingKeeper.GetUnbondingDelegationsFromValidator(ctx, valOperatorAddress) { - for _, entry := range v.Entries { - undelegationsInTokens = undelegationsInTokens.Add(entry.InitialBalance) - } - } - - redelegationsInTokens := sdk.NewInt(0) - for _, v := range k.stakingKeeper.GetRedelegationsFromSrcValidator(ctx, valOperatorAddress) { - for _, entry := range v.Entries { - redelegationsInTokens = redelegationsInTokens.Add(entry.InitialBalance) - } - } - - powerReduction := k.stakingKeeper.PowerReduction(ctx) - undelegationsAndRedelegationsInPower := sdk.TokensToConsensusPower( - undelegationsInTokens.Add(redelegationsInTokens), powerReduction) - - power := k.stakingKeeper.GetLastValidatorPower(ctx, valOperatorAddress) - totalPower := power + undelegationsAndRedelegationsInPower - slashFraction := k.slashingKeeper.SlashFractionDoubleSign(ctx) - - // TODO: what if it's another key ???? - k.stakingKeeper.Slash(ctx, providerAddr.ToSdkConsAddr(), 0, totalPower, slashFraction, stakingtypes.DoubleSign) -} - // 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( @@ -70,7 +33,7 @@ func (k Keeper) HandleConsumerDoubleVoting( ) // execute the jailing - k.slashValidator(ctx, providerAddr) + k.SlashValidator(ctx, providerAddr) k.JailAndTombstoneValidator(ctx, providerAddr) k.Logger(ctx).Info( diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 40ae2c6f11..92d4acbadc 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -41,7 +41,7 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty misbehaviour.Header1.Header.ChainID, types.NewConsumerConsAddress(sdk.ConsAddress(v.Address.Bytes())), ) - k.slashValidator(ctx, providerAddr) + k.SlashValidator(ctx, providerAddr) k.JailAndTombstoneValidator(ctx, providerAddr) provAddrs = append(provAddrs, providerAddr) } diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index 9323128a60..eedf41a1b1 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -3,26 +3,24 @@ package keeper import ( 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" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" ) -// JailAndTombstoneValidator 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. +// JailAndTombstoneValidator jails and tombstones the validator with the given provider consensus address func (k Keeper) JailAndTombstoneValidator(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()) + logger.Error("validator not found or is unbonded", "provider consensus address", 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()) + logger.Info("validator is already tombstoned", "provider consensus address", providerAddr.String()) return } @@ -31,9 +29,62 @@ func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.Pr k.stakingKeeper.Jail(ctx, providerAddr.ToSdkConsAddr()) } - // update jail time to end after double sign jail duration + // Jail the validator to trigger the unbonding of the validator + // (see cosmos/cosmos-sdk/blob/v0.47.0/x/staking/keeper/val_state_change.go#L194). k.slashingKeeper.JailUntil(ctx, providerAddr.ToSdkConsAddr(), evidencetypes.DoubleSignJailEndTime) - // TODO: do we need to jail if we tombstone, that's what cosmos-sdk does + // Tombstone the validator so that we cannot slash the validator more than once + // (see cosmos/cosmos-sdk/blob/v0.47.0/x/evidence/keeper/infraction.go#L94). + // Note that we cannot simply use the fact that a validator is jailed to avoid slashing more than once + // because then a validator could i) perform an equivocation, ii) get jailed (e.g., through downtime) + // and in such a case the validator would not get slashed when calling `SlashValidator`. k.slashingKeeper.Tombstone(ctx, providerAddr.ToSdkConsAddr()) + + //k.evidenceKeeper.SetEvidence(ctx) +} + +// Slash validator based on the `providerAddr` +func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) { + logger := k.Logger(ctx) + + val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) + if !found { + logger.Error("validator not found", "provider consensus address", providerAddr.String()) + return + } + + if k.slashingKeeper.IsTombstoned(ctx, providerAddr.ToSdkConsAddr()) { + logger.Info("validator is already tombstoned", "provider consensus address", providerAddr.String()) + return + } + + valOperatorAddress := val.GetOperator() + + // compute the total numbers of tokens currently being undelegated + undelegationsInTokens := sdk.NewInt(0) + for _, v := range k.stakingKeeper.GetUnbondingDelegationsFromValidator(ctx, valOperatorAddress) { + for _, entry := range v.Entries { + undelegationsInTokens = undelegationsInTokens.Add(entry.InitialBalance) + } + } + + // compute the total numbers of tokens currently being redelegated + redelegationsInTokens := sdk.NewInt(0) + for _, v := range k.stakingKeeper.GetRedelegationsFromSrcValidator(ctx, valOperatorAddress) { + for _, entry := range v.Entries { + redelegationsInTokens = redelegationsInTokens.Add(entry.InitialBalance) + } + } + + // The power we pass to staking's keeper `Slash` method is the current power of the validator together with the total + // power of all the currently undelegated and redelegated tokens (see docs/docs/adrs/adr-013-equivocation-slashing.md). + powerReduction := k.stakingKeeper.PowerReduction(ctx) + undelegationsAndRedelegationsInPower := sdk.TokensToConsensusPower( + undelegationsInTokens.Add(redelegationsInTokens), powerReduction) + + power := k.stakingKeeper.GetLastValidatorPower(ctx, valOperatorAddress) + totalPower := power + undelegationsAndRedelegationsInPower + slashFraction := k.slashingKeeper.SlashFractionDoubleSign(ctx) + + k.stakingKeeper.Slash(ctx, providerAddr.ToSdkConsAddr(), 0, totalPower, slashFraction, stakingtypes.DoubleSign) } diff --git a/x/ccv/types/expected_keepers.go b/x/ccv/types/expected_keepers.go index bc55ebbf7e..6cf132e5d4 100644 --- a/x/ccv/types/expected_keepers.go +++ b/x/ccv/types/expected_keepers.go @@ -2,6 +2,7 @@ package types import ( context "context" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -50,6 +51,7 @@ type StakingKeeper interface { type EvidenceKeeper interface { HandleEquivocationEvidence(ctx sdk.Context, evidence *evidencetypes.Equivocation) + SetEvidence(ctx sdk.Context, evidence exported.Evidence) } // SlashingKeeper defines the contract expected to perform ccv slashing From e1bdfff2ab141e273c3f4cfd412b170064a8ea20 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Mon, 11 Sep 2023 11:54:27 +0200 Subject: [PATCH 04/29] add tests that checks that Slash is called --- tests/integration/double_vote.go | 2 +- x/ccv/provider/keeper/double_vote.go | 13 +- x/ccv/provider/keeper/misbehaviour.go | 17 +- x/ccv/provider/keeper/punish_validator.go | 63 +++--- .../provider/keeper/punish_validator_test.go | 201 +++++++++++++++++- 5 files changed, 263 insertions(+), 33 deletions(-) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index 2eca555678..0f3b537e53 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -10,7 +10,7 @@ import ( ) // TestHandleConsumerDoubleVoting verifies that handling a double voting evidence -// of a consumer chain results in the expected jailing of the malicious validator +// of a consumer chain results in the expected tombstoning and jailing of the malicious validator func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { s.SetupCCVChannel(s.path) // required to have the consumer client revision height greater than 0 diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go index d4f13fe671..0fa03c3fc0 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -4,6 +4,8 @@ import ( "bytes" "fmt" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -32,10 +34,19 @@ func (k Keeper) HandleConsumerDoubleVoting( types.NewConsumerConsAddress(sdk.ConsAddress(evidence.VoteA.ValidatorAddress.Bytes())), ) - // execute the jailing k.SlashValidator(ctx, providerAddr) k.JailAndTombstoneValidator(ctx, providerAddr) + // verify the following values + var equivocation exported.Evidence = &evidencetypes.Equivocation{ + Height: evidence.Height(), + Time: evidence.Time(), + Power: evidence.ValidatorPower, + ConsensusAddress: evidence.VoteA.ValidatorAddress.String(), + } + + k.evidenceKeeper.SetEvidence(ctx, equivocation) + k.Logger(ctx).Info( "confirmed equivocation", "byzantine validator address", providerAddr.String(), diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 92d4acbadc..2be3c63cf2 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -1,12 +1,13 @@ package keeper import ( - "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" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + 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" + "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" tmtypes "github.com/tendermint/tendermint/types" ) @@ -46,6 +47,18 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty provAddrs = append(provAddrs, providerAddr) } + // FIXME: it would be nice to set the evidence but not sure what this would mean + // for light client attacks. + var evidence exported.Evidence = &evidencetypes.Equivocation{ + // FIXME + Height: int64(misbehaviour.Header1.GetHeight().GetRevisionHeight()), + Time: misbehaviour.Header2.GetTime(), + Power: 0, // misbehaviour.Header1.ValidatorSet.Validators + ConsensusAddress: "hola!", //misbehaviour.Header1.ValidatorSet.Proposer.Address + } + + k.evidenceKeeper.SetEvidence(ctx, evidence) + logger.Info( "confirmed equivocation light client attack", "byzantine validators", provAddrs, diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index eedf41a1b1..915261f33a 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -1,6 +1,7 @@ package keeper import ( + "fmt" 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" @@ -38,52 +39,64 @@ func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.Pr // Note that we cannot simply use the fact that a validator is jailed to avoid slashing more than once // because then a validator could i) perform an equivocation, ii) get jailed (e.g., through downtime) // and in such a case the validator would not get slashed when calling `SlashValidator`. + // TODO: check if tombstone ... can it panic??? k.slashingKeeper.Tombstone(ctx, providerAddr.ToSdkConsAddr()) - - //k.evidenceKeeper.SetEvidence(ctx) } -// Slash validator based on the `providerAddr` -func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) { - logger := k.Logger(ctx) - - val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) - if !found { - logger.Error("validator not found", "provider consensus address", providerAddr.String()) - return - } - - if k.slashingKeeper.IsTombstoned(ctx, providerAddr.ToSdkConsAddr()) { - logger.Info("validator is already tombstoned", "provider consensus address", providerAddr.String()) - return - } - - valOperatorAddress := val.GetOperator() +func (k Keeper) ComputePowerToSlash(undelegations []stakingtypes.UnbondingDelegation, + redelegations []stakingtypes.Redelegation, power int64, powerReduction sdk.Int) int64 { // compute the total numbers of tokens currently being undelegated undelegationsInTokens := sdk.NewInt(0) - for _, v := range k.stakingKeeper.GetUnbondingDelegationsFromValidator(ctx, valOperatorAddress) { - for _, entry := range v.Entries { + for _, u := range undelegations { + for _, entry := range u.Entries { undelegationsInTokens = undelegationsInTokens.Add(entry.InitialBalance) } } // compute the total numbers of tokens currently being redelegated redelegationsInTokens := sdk.NewInt(0) - for _, v := range k.stakingKeeper.GetRedelegationsFromSrcValidator(ctx, valOperatorAddress) { - for _, entry := range v.Entries { + for _, r := range redelegations { + for _, entry := range r.Entries { redelegationsInTokens = redelegationsInTokens.Add(entry.InitialBalance) } } // The power we pass to staking's keeper `Slash` method is the current power of the validator together with the total // power of all the currently undelegated and redelegated tokens (see docs/docs/adrs/adr-013-equivocation-slashing.md). - powerReduction := k.stakingKeeper.PowerReduction(ctx) undelegationsAndRedelegationsInPower := sdk.TokensToConsensusPower( undelegationsInTokens.Add(redelegationsInTokens), powerReduction) - power := k.stakingKeeper.GetLastValidatorPower(ctx, valOperatorAddress) - totalPower := power + undelegationsAndRedelegationsInPower + return power + undelegationsAndRedelegationsInPower +} + +// Slash validator based on the `providerAddr` +func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) { + logger := k.Logger(ctx) + + fmt.Println(">>>>") + fmt.Println(ctx) + fmt.Println("===") + fmt.Println(providerAddr) + fmt.Println(providerAddr.ToSdkConsAddr().Bytes()) + fmt.Println(">>>>") + val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) + if !found { + logger.Error("validator not found", "provider consensus address", providerAddr.String()) + return + } + + if k.slashingKeeper.IsTombstoned(ctx, providerAddr.ToSdkConsAddr()) { + logger.Info("validator is already tombstoned", "provider consensus address", providerAddr.String()) + return + } + + undelegations := k.stakingKeeper.GetUnbondingDelegationsFromValidator(ctx, val.GetOperator()) + redelegations := k.stakingKeeper.GetRedelegationsFromSrcValidator(ctx, val.GetOperator()) + lastPower := k.stakingKeeper.GetLastValidatorPower(ctx, val.GetOperator()) + powerReduction := k.stakingKeeper.PowerReduction(ctx) + totalPower := k.ComputePowerToSlash(undelegations, redelegations, lastPower, powerReduction) + slashFraction := k.slashingKeeper.SlashFractionDoubleSign(ctx) k.stakingKeeper.Slash(ctx, providerAddr.ToSdkConsAddr(), 0, totalPower, slashFraction, stakingtypes.DoubleSign) diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index 286da69fe2..67b2bb6035 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -1,8 +1,8 @@ package keeper_test import ( - "testing" - + "fmt" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" 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" @@ -10,11 +10,16 @@ import ( testkeeper "github.com/cosmos/interchain-security/v2/testutil/keeper" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + tmtypes "github.com/tendermint/tendermint/types" + "testing" + "time" ) -// TestJailValidator tests that the jailing of a validator is only executed +// FIXME: break the JailAndTombstoneValidator function into two ... seems complicated to do both at once +// TestJailAndTombstoneValidator 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) { +func TestJailAndTombstoneValidator(t *testing.T) { providerConsAddr := cryptotestutil.NewCryptoIdentityFromIntSeed(7842334).ProviderConsAddress() testCases := []struct { name string @@ -130,3 +135,191 @@ func TestJailValidator(t *testing.T) { ctrl.Finish() } } + +func createUndelegation(tokensPerEntry []int64) stakingtypes.UnbondingDelegation { + var entries []stakingtypes.UnbondingDelegationEntry + for _, t := range tokensPerEntry { + entry := stakingtypes.UnbondingDelegationEntry{ + InitialBalance: sdk.NewInt(t), + } + entries = append(entries, entry) + } + + return stakingtypes.UnbondingDelegation{Entries: entries} +} + +func createRedelegation(tokensPerEntry []int64) stakingtypes.Redelegation { + var entries []stakingtypes.RedelegationEntry + for _, t := range tokensPerEntry { + entry := stakingtypes.RedelegationEntry{ + InitialBalance: sdk.NewInt(t), + } + entries = append(entries, entry) + } + + return stakingtypes.Redelegation{Entries: entries} +} + +func TestComputePowerToSlash(t *testing.T) { + providerKeeper, _, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + testCases := []struct { + name string + undelegations []stakingtypes.UnbondingDelegation + redelegations []stakingtypes.Redelegation + power int64 + powerReduction sdk.Int + expectedPower int64 + }{ + { + "both undelegations and redelegations 1", + // 1000 total undelegation tokens + []stakingtypes.UnbondingDelegation{ + createUndelegation([]int64{250, 250}), + createUndelegation([]int64{500})}, + // 1000 total redelegation tokens + []stakingtypes.Redelegation{ + createRedelegation([]int64{500}), + createRedelegation([]int64{250, 250}), + }, + int64(1000), + sdk.NewInt(1), + int64(2000/1 + 1000), + }, + { + "both undelegations and redelegations 2", + // 2000 total undelegation tokens + []stakingtypes.UnbondingDelegation{ + createUndelegation([]int64{250, 250}), + createUndelegation([]int64{}), + createUndelegation([]int64{100, 100}), + createUndelegation([]int64{800}), + createUndelegation([]int64{500})}, + // 3500 total redelegation tokens + []stakingtypes.Redelegation{ + createRedelegation([]int64{}), + createRedelegation([]int64{1600}), + createRedelegation([]int64{350, 250}), + createRedelegation([]int64{700, 200}), + createRedelegation([]int64{}), + createRedelegation([]int64{400}), + }, + int64(8391), + sdk.NewInt(2), + int64((2000+3500)/2 + 8391), + }, + { + "no undelegations or redelegations, return provided power", + []stakingtypes.UnbondingDelegation{}, + []stakingtypes.Redelegation{}, + int64(3000), + sdk.NewInt(5), + int64(0/5 + 3000), + }, + { + "no undelegations", + []stakingtypes.UnbondingDelegation{}, + // 2000 total redelegation tokens + []stakingtypes.Redelegation{ + createRedelegation([]int64{}), + createRedelegation([]int64{500}), + createRedelegation([]int64{250, 250}), + createRedelegation([]int64{700, 200}), + createRedelegation([]int64{}), + createRedelegation([]int64{100}), + }, + int64(17), + sdk.NewInt(3), + int64(2000/3 + 17), + }, + { + "no redelegations", + // 2000 total undelegation tokens + []stakingtypes.UnbondingDelegation{ + createUndelegation([]int64{250, 250}), + createUndelegation([]int64{}), + createUndelegation([]int64{100, 100}), + createUndelegation([]int64{800}), + createUndelegation([]int64{500})}, + []stakingtypes.Redelegation{}, + int64(1), + sdk.NewInt(3), + int64(2000/3 + 1), + }, + } + + for _, tc := range testCases { + actualPower := providerKeeper.ComputePowerToSlash(tc.undelegations, tc.redelegations, tc.power, tc.powerReduction) + if tc.expectedPower != actualPower { + require.Fail(t, fmt.Sprintf("\"%s\" failed", tc.name), + "expected is %d but actual is %d", tc.expectedPower, actualPower) + } + } +} + +// TestSlashValidator asserts that `SlashValidator` calls the staking module's `Slash` method +// with the correct arguments (i.e., `infractionHeight` of 0 and the expected slash power) +func TestSlashValidator(t *testing.T) { + keeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + ctx = ctx.WithBlockTime(time.Now()) + keeperParams := testkeeper.NewInMemKeeperParams(t) + testkeeper.NewInMemProviderKeeper(keeperParams, mocks) + + pubKey, _ := cryptocodec.FromTmPubKeyInterface(tmtypes.NewMockPV().PrivKey.PubKey()) + + validator, _ := stakingtypes.NewValidator(pubKey.Address().Bytes(), pubKey, stakingtypes.Description{}) + consAddr, _ := validator.GetConsAddr() + providerAddr := types.NewProviderConsAddress(consAddr) + + // we create 1000 tokens worth of undelegations and 1000 tokens worth of redelegations + undelegations := []stakingtypes.UnbondingDelegation{ + createUndelegation([]int64{250, 250}), + createUndelegation([]int64{500})} + redelegations := []stakingtypes.Redelegation{ + createRedelegation([]int64{250, 250}), + createRedelegation([]int64{500})} + + // validator's current power + currentPower := int64(3000) + + powerReduction := sdk.NewInt(2) + slashFraction, _ := sdk.NewDecFromStr("0.5") + + // the call to `Slash` should provide an `infractionHeight` of 0 and an expected power of + // (1000 (undelegations) + 1000 (redelegations)) / 2 (= powerReduction) + 3000 (currentPower) = 4000 + expectedInfractionHeight := int64(0) + expectedSlashPower := int64(4000) + + expectedCalls := []*gomock.Call{ + mocks.MockStakingKeeper.EXPECT(). + GetValidatorByConsAddr(ctx, gomock.Any()). + Return(validator, true), + mocks.MockSlashingKeeper.EXPECT(). + IsTombstoned(ctx, consAddr). + Return(false), + mocks.MockStakingKeeper.EXPECT(). + GetUnbondingDelegationsFromValidator(ctx, validator.GetOperator()). + Return(undelegations), + mocks.MockStakingKeeper.EXPECT(). + GetRedelegationsFromSrcValidator(ctx, validator.GetOperator()). + Return(redelegations), + mocks.MockStakingKeeper.EXPECT(). + GetLastValidatorPower(ctx, validator.GetOperator()). + Return(currentPower), + mocks.MockStakingKeeper.EXPECT(). + PowerReduction(ctx). + Return(powerReduction), + mocks.MockSlashingKeeper.EXPECT(). + SlashFractionDoubleSign(ctx). + Return(slashFraction), + mocks.MockStakingKeeper.EXPECT(). + Slash(ctx, consAddr, expectedInfractionHeight, expectedSlashPower, slashFraction, stakingtypes.DoubleSign). + Times(1), + } + + gomock.InOrder(expectedCalls...) + keeper.SlashValidator(ctx, providerAddr) +} From bbff2eef05bedf0fd613623fe83a0e6d2ee747f5 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Mon, 11 Sep 2023 12:42:01 +0200 Subject: [PATCH 05/29] add FIXME msg --- x/ccv/provider/keeper/punish_validator_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index 67b2bb6035..1fb0d69a4d 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -258,6 +258,9 @@ func TestComputePowerToSlash(t *testing.T) { } } +// FIXME: Probably the following test could be a combination with computePowertoSlash ... +// Not sure I like this +// the actual slashing should be checked from integration tests // TestSlashValidator asserts that `SlashValidator` calls the staking module's `Slash` method // with the correct arguments (i.e., `infractionHeight` of 0 and the expected slash power) func TestSlashValidator(t *testing.T) { From dfef4100321a9beb889540c80e21086d1e5ff0eb Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Mon, 11 Sep 2023 21:57:01 +0200 Subject: [PATCH 06/29] small changes --- tests/integration/double_vote.go | 11 ++++++++++- tests/integration/misbehaviour.go | 11 ++++++++++- x/ccv/provider/keeper/punish_validator.go | 7 ------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index 0f3b537e53..aff5e5cae9 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -10,7 +10,7 @@ import ( ) // TestHandleConsumerDoubleVoting verifies that handling a double voting evidence -// of a consumer chain results in the expected tombstoning and jailing of the malicious validator +// of a consumer chain results in the expected tombstoning, jailing, and slashing of the malicious validator func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { s.SetupCCVChannel(s.path) // required to have the consumer client revision height greater than 0 @@ -159,6 +159,9 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { consuAddr := types.NewConsumerConsAddress(sdk.ConsAddress(consuVal.Address.Bytes())) provAddr := s.providerApp.GetProviderKeeper().GetProviderAddrFromConsumerAddr(s.providerCtx(), s.consumerChain.ChainID, consuAddr) + validator, _ := s.providerApp.GetTestStakingKeeper().GetValidator(s.providerCtx(), provAddr.ToSdkConsAddr().Bytes()) + initialTokens := validator.GetTokens().ToDec() + for _, tc := range testCases { s.Run(tc.name, func() { // reset context for each run @@ -188,6 +191,12 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { // verifies that the tombstoning and jailing has occurred s.Require().True(s.providerApp.GetTestSlashingKeeper().IsTombstoned(provCtx, provAddr.ToSdkConsAddr())) s.Require().True(s.providerApp.GetTestStakingKeeper().IsValidatorJailed(provCtx, provAddr.ToSdkConsAddr())) + + // verifies that the validator gets slashed and has fewer tokens after the slashing + validator, _ := s.providerApp.GetTestStakingKeeper().GetValidator(provCtx, provAddr.ToSdkConsAddr().Bytes()) + slashFraction := s.providerApp.GetTestSlashingKeeper().SlashFractionDoubleSign(provCtx) + actualTokens := validator.GetTokens().ToDec() + s.Require().True(initialTokens.Sub(initialTokens.Mul(slashFraction)).Equal(actualTokens)) } else { s.Require().Error(err) diff --git a/tests/integration/misbehaviour.go b/tests/integration/misbehaviour.go index 66e1388474..caa0647542 100644 --- a/tests/integration/misbehaviour.go +++ b/tests/integration/misbehaviour.go @@ -53,10 +53,14 @@ func (s *CCVTestSuite) TestHandleConsumerMisbehaviour() { ), } + // we assume that all validators have the same number of initial tokens + validator, _ := s.getValByIdx(0) + initialTokens := validator.GetTokens().ToDec() + err := s.providerApp.GetProviderKeeper().HandleConsumerMisbehaviour(s.providerCtx(), *misb) s.NoError(err) - // verify that validators are jailed and tombstoned + // verify that validators are jailed, tombstoned, and slashed for _, v := range clientTMValset.Validators { consuAddr := sdk.ConsAddress(v.Address.Bytes()) provAddr := s.providerApp.GetProviderKeeper().GetProviderAddrFromConsumerAddr(s.providerCtx(), s.consumerChain.ChainID, types.NewConsumerConsAddress(consuAddr)) @@ -64,6 +68,11 @@ func (s *CCVTestSuite) TestHandleConsumerMisbehaviour() { s.Require().True(ok) s.Require().True(val.Jailed) s.Require().True(s.providerApp.GetTestSlashingKeeper().IsTombstoned(s.providerCtx(), provAddr.ToSdkConsAddr())) + + validator, _ := s.providerApp.GetTestStakingKeeper().GetValidator(s.providerCtx(), provAddr.ToSdkConsAddr().Bytes()) + slashFraction := s.providerApp.GetTestSlashingKeeper().SlashFractionDoubleSign(s.providerCtx()) + actualTokens := validator.GetTokens().ToDec() + s.Require().True(initialTokens.Sub(initialTokens.Mul(slashFraction)).Equal(actualTokens)) } } diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index 915261f33a..3de17eafa2 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -1,7 +1,6 @@ package keeper import ( - "fmt" 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" @@ -74,12 +73,6 @@ func (k Keeper) ComputePowerToSlash(undelegations []stakingtypes.UnbondingDelega func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) { logger := k.Logger(ctx) - fmt.Println(">>>>") - fmt.Println(ctx) - fmt.Println("===") - fmt.Println(providerAddr) - fmt.Println(providerAddr.ToSdkConsAddr().Bytes()) - fmt.Println(">>>>") val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) if !found { logger.Error("validator not found", "provider consensus address", providerAddr.String()) From 8b49af108b474bc7b5580b942fc783dc0cd62493 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Mon, 11 Sep 2023 22:19:32 +0200 Subject: [PATCH 07/29] add fixme --- x/ccv/provider/keeper/double_vote.go | 2 +- x/ccv/provider/keeper/misbehaviour.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go index 0fa03c3fc0..01f2536df4 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -37,7 +37,7 @@ func (k Keeper) HandleConsumerDoubleVoting( k.SlashValidator(ctx, providerAddr) k.JailAndTombstoneValidator(ctx, providerAddr) - // verify the following values + // FIXME: verify the following values var equivocation exported.Evidence = &evidencetypes.Equivocation{ Height: evidence.Height(), Time: evidence.Time(), diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 2be3c63cf2..ff2ed83dca 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -50,11 +50,10 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty // FIXME: it would be nice to set the evidence but not sure what this would mean // for light client attacks. var evidence exported.Evidence = &evidencetypes.Equivocation{ - // FIXME Height: int64(misbehaviour.Header1.GetHeight().GetRevisionHeight()), Time: misbehaviour.Header2.GetTime(), - Power: 0, // misbehaviour.Header1.ValidatorSet.Validators - ConsensusAddress: "hola!", //misbehaviour.Header1.ValidatorSet.Proposer.Address + Power: 0, + ConsensusAddress: "!", } k.evidenceKeeper.SetEvidence(ctx, evidence) From bf8950926e885ff9cf77da56b94907920029d6f9 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Mon, 11 Sep 2023 22:44:25 +0200 Subject: [PATCH 08/29] fix test --- x/ccv/provider/keeper/punish_validator_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index 1fb0d69a4d..2f101c2376 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -93,6 +93,9 @@ func TestJailAndTombstoneValidator(t *testing.T) { mocks.MockSlashingKeeper.EXPECT().JailUntil( ctx, providerConsAddr.ToSdkConsAddr(), evidencetypes.DoubleSignJailEndTime). Times(1), + mocks.MockSlashingKeeper.EXPECT().Tombstone( + ctx, providerConsAddr.ToSdkConsAddr()). + Times(1), } }, }, @@ -117,6 +120,9 @@ func TestJailAndTombstoneValidator(t *testing.T) { mocks.MockSlashingKeeper.EXPECT().JailUntil( ctx, providerConsAddr.ToSdkConsAddr(), evidencetypes.DoubleSignJailEndTime). Times(1), + mocks.MockSlashingKeeper.EXPECT().Tombstone( + ctx, providerConsAddr.ToSdkConsAddr()). + Times(1), } }, }, From 868e0d256b5512d511451aab366c5868e24c8f79 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Tue, 12 Sep 2023 15:23:42 +0200 Subject: [PATCH 09/29] modified E2E tests and general cleaning up --- tests/e2e/steps_consumer_misbehaviour.go | 12 +- tests/e2e/steps_double_sign.go | 19 ++- tests/integration/double_vote.go | 7 +- x/ccv/provider/keeper/double_vote.go | 13 -- x/ccv/provider/keeper/misbehaviour.go | 13 -- x/ccv/provider/keeper/punish_validator.go | 21 ++- .../provider/keeper/punish_validator_test.go | 130 +++++++++++------- x/ccv/types/expected_keepers.go | 2 - 8 files changed, 131 insertions(+), 86 deletions(-) diff --git a/tests/e2e/steps_consumer_misbehaviour.go b/tests/e2e/steps_consumer_misbehaviour.go index 53cfb78fae..39caaa93da 100644 --- a/tests/e2e/steps_consumer_misbehaviour.go +++ b/tests/e2e/steps_consumer_misbehaviour.go @@ -219,7 +219,7 @@ func stepsCauseConsumerMisbehaviour(consumerName string) []Step { state: State{}, }, // detect the ICS misbehaviour - // and jail alice on the provider + // and jail and slash alice on the provider { action: detectConsumerEvidenceAction{ chain: chainID(consumerName), @@ -230,6 +230,10 @@ func stepsCauseConsumerMisbehaviour(consumerName string) []Step { validatorID("alice"): 511, validatorID("bob"): 20, }, + RepresentativePowers: &map[validatorID]uint{ + validatorID("alice"): 511000000, + validatorID("bob"): 20000000, + }, }, chainID(consumerName): ChainState{ ValPowers: &map[validatorID]uint{ @@ -255,6 +259,12 @@ func stepsCauseConsumerMisbehaviour(consumerName string) []Step { validatorID("alice"): 0, validatorID("bob"): 20, }, + // "alice" should be slashed on the provider, hence representative + // power is 511000000 - 0.05 * 511000000 = 485450000 + RepresentativePowers: &map[validatorID]uint{ + validatorID("alice"): 485450000, + validatorID("bob"): 20000000, + }, // The consumer light client should be frozen on the provider ClientsFrozenHeights: &map[string]clienttypes.Height{ consumerClientID: { diff --git a/tests/e2e/steps_double_sign.go b/tests/e2e/steps_double_sign.go index dcce236b46..4a29a475a7 100644 --- a/tests/e2e/steps_double_sign.go +++ b/tests/e2e/steps_double_sign.go @@ -144,6 +144,11 @@ func stepsCauseDoubleSignOnConsumer(consumerName, providerName string) []Step { validatorID("bob"): 500, validatorID("carol"): 500, }, + RepresentativePowers: &map[validatorID]uint{ + validatorID("alice"): 500000000, + validatorID("bob"): 500000000, + validatorID("carol"): 500000000, + }, }, chainID(consumerName): ChainState{ ValPowers: &map[validatorID]uint{ @@ -155,7 +160,7 @@ func stepsCauseDoubleSignOnConsumer(consumerName, providerName string) []Step { }, }, // detect the double voting infraction - // and jail bob on the provider + // and jail and slashing of bob on the provider { action: detectConsumerEvidenceAction{ chain: chainID(consumerName), @@ -167,6 +172,13 @@ func stepsCauseDoubleSignOnConsumer(consumerName, providerName string) []Step { validatorID("bob"): 0, validatorID("carol"): 500, }, + // "bob" gets slashed on the provider chain, hence representative + // power is 500000000 - 0.05 * 500000000 = 475000000 + RepresentativePowers: &map[validatorID]uint{ + validatorID("alice"): 500000000, + validatorID("bob"): 475000000, + validatorID("carol"): 500000000, + }, }, chainID(consumerName): ChainState{ ValPowers: &map[validatorID]uint{ @@ -192,6 +204,11 @@ func stepsCauseDoubleSignOnConsumer(consumerName, providerName string) []Step { validatorID("bob"): 0, validatorID("carol"): 500, }, + RepresentativePowers: &map[validatorID]uint{ + validatorID("alice"): 500000000, + validatorID("bob"): 475000000, + validatorID("carol"): 500000000, + }, }, chainID(consumerName): ChainState{ ValPowers: &map[validatorID]uint{ diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index aff5e5cae9..cc576055bd 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -188,9 +188,9 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { if tc.expPass { s.Require().NoError(err) - // verifies that the tombstoning and jailing has occurred - s.Require().True(s.providerApp.GetTestSlashingKeeper().IsTombstoned(provCtx, provAddr.ToSdkConsAddr())) + // verifies that the jailing and tombstoning has occurred s.Require().True(s.providerApp.GetTestStakingKeeper().IsValidatorJailed(provCtx, provAddr.ToSdkConsAddr())) + s.Require().True(s.providerApp.GetTestSlashingKeeper().IsTombstoned(provCtx, provAddr.ToSdkConsAddr())) // verifies that the validator gets slashed and has fewer tokens after the slashing validator, _ := s.providerApp.GetTestStakingKeeper().GetValidator(provCtx, provAddr.ToSdkConsAddr().Bytes()) @@ -200,8 +200,9 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { } else { s.Require().Error(err) - // verifies that no jailing and has occurred + // verifies that no jailing and no tombstoning has occurred s.Require().False(s.providerApp.GetTestStakingKeeper().IsValidatorJailed(provCtx, provAddr.ToSdkConsAddr())) + s.Require().False(s.providerApp.GetTestSlashingKeeper().IsTombstoned(provCtx, provAddr.ToSdkConsAddr())) } }) } diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go index 01f2536df4..62ddaf408d 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -4,9 +4,6 @@ import ( "bytes" "fmt" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - "github.com/cosmos/cosmos-sdk/x/evidence/exported" - evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/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" @@ -37,16 +34,6 @@ func (k Keeper) HandleConsumerDoubleVoting( k.SlashValidator(ctx, providerAddr) k.JailAndTombstoneValidator(ctx, providerAddr) - // FIXME: verify the following values - var equivocation exported.Evidence = &evidencetypes.Equivocation{ - Height: evidence.Height(), - Time: evidence.Time(), - Power: evidence.ValidatorPower, - ConsensusAddress: evidence.VoteA.ValidatorAddress.String(), - } - - k.evidenceKeeper.SetEvidence(ctx, equivocation) - k.Logger(ctx).Info( "confirmed equivocation", "byzantine validator address", providerAddr.String(), diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index ff2ed83dca..1a0261b800 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -3,8 +3,6 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/cosmos/cosmos-sdk/x/evidence/exported" - 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" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" @@ -47,17 +45,6 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty provAddrs = append(provAddrs, providerAddr) } - // FIXME: it would be nice to set the evidence but not sure what this would mean - // for light client attacks. - var evidence exported.Evidence = &evidencetypes.Equivocation{ - Height: int64(misbehaviour.Header1.GetHeight().GetRevisionHeight()), - Time: misbehaviour.Header2.GetTime(), - Power: 0, - ConsensusAddress: "!", - } - - k.evidenceKeeper.SetEvidence(ctx, evidence) - logger.Info( "confirmed equivocation light client attack", "byzantine validators", provAddrs, diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index 3de17eafa2..b0ae53deda 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -5,6 +5,7 @@ import ( evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" + "time" ) // JailAndTombstoneValidator jails and tombstones the validator with the given provider consensus address @@ -34,21 +35,26 @@ func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.Pr k.slashingKeeper.JailUntil(ctx, providerAddr.ToSdkConsAddr(), evidencetypes.DoubleSignJailEndTime) // Tombstone the validator so that we cannot slash the validator more than once - // (see cosmos/cosmos-sdk/blob/v0.47.0/x/evidence/keeper/infraction.go#L94). + // (see cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/x/evidence/keeper/infraction.go#L81). // Note that we cannot simply use the fact that a validator is jailed to avoid slashing more than once // because then a validator could i) perform an equivocation, ii) get jailed (e.g., through downtime) - // and in such a case the validator would not get slashed when calling `SlashValidator`. - // TODO: check if tombstone ... can it panic??? + // and in such a case the validator would not get slashed when we call `SlashValidator`. k.slashingKeeper.Tombstone(ctx, providerAddr.ToSdkConsAddr()) } -func (k Keeper) ComputePowerToSlash(undelegations []stakingtypes.UnbondingDelegation, +// ComputePowerToSlash computes the power to be slashed based on the tokens in non-matured (based on the +// provider `now` time) `undelegations` and `redelegations`, as well as the current `power` of the validator +func (k Keeper) ComputePowerToSlash(now time.Time, undelegations []stakingtypes.UnbondingDelegation, redelegations []stakingtypes.Redelegation, power int64, powerReduction sdk.Int) int64 { // compute the total numbers of tokens currently being undelegated undelegationsInTokens := sdk.NewInt(0) for _, u := range undelegations { for _, entry := range u.Entries { + if entry.IsMature(now) && !entry.OnHold() { + // undelegation no longer eligible for slashing, skip it + continue + } undelegationsInTokens = undelegationsInTokens.Add(entry.InitialBalance) } } @@ -57,6 +63,10 @@ func (k Keeper) ComputePowerToSlash(undelegations []stakingtypes.UnbondingDelega redelegationsInTokens := sdk.NewInt(0) for _, r := range redelegations { for _, entry := range r.Entries { + if entry.IsMature(now) && !entry.OnHold() { + // redelegation no longer eligible for slashing, skip it + continue + } redelegationsInTokens = redelegationsInTokens.Add(entry.InitialBalance) } } @@ -88,8 +98,7 @@ func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsA redelegations := k.stakingKeeper.GetRedelegationsFromSrcValidator(ctx, val.GetOperator()) lastPower := k.stakingKeeper.GetLastValidatorPower(ctx, val.GetOperator()) powerReduction := k.stakingKeeper.PowerReduction(ctx) - totalPower := k.ComputePowerToSlash(undelegations, redelegations, lastPower, powerReduction) - + totalPower := k.ComputePowerToSlash(ctx.BlockHeader().Time, undelegations, redelegations, lastPower, powerReduction) slashFraction := k.slashingKeeper.SlashFractionDoubleSign(ctx) k.stakingKeeper.Slash(ctx, providerAddr.ToSdkConsAddr(), 0, totalPower, slashFraction, stakingtypes.DoubleSign) diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index 2f101c2376..ec5ba0d8ab 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -16,9 +16,8 @@ import ( "time" ) -// FIXME: break the JailAndTombstoneValidator function into two ... seems complicated to do both at once // TestJailAndTombstoneValidator tests that the jailing of a validator is only executed -// under the conditions that the validator is neither unbonded, already jailed, nor tombstoned. +// under the conditions that the validator is neither unbonded, nor jailed, nor tombstoned. func TestJailAndTombstoneValidator(t *testing.T) { providerConsAddr := cryptotestutil.NewCryptoIdentityFromIntSeed(7842334).ProviderConsAddress() testCases := []struct { @@ -142,11 +141,13 @@ func TestJailAndTombstoneValidator(t *testing.T) { } } -func createUndelegation(tokensPerEntry []int64) stakingtypes.UnbondingDelegation { +// createUndelegation creates an undelegation with `len(initialBalances)` entries +func createUndelegation(initialBalances []int64, completionTimes []time.Time) stakingtypes.UnbondingDelegation { var entries []stakingtypes.UnbondingDelegationEntry - for _, t := range tokensPerEntry { + for i, balance := range initialBalances { entry := stakingtypes.UnbondingDelegationEntry{ - InitialBalance: sdk.NewInt(t), + InitialBalance: sdk.NewInt(balance), + CompletionTime: completionTimes[i], } entries = append(entries, entry) } @@ -154,11 +155,13 @@ func createUndelegation(tokensPerEntry []int64) stakingtypes.UnbondingDelegation return stakingtypes.UnbondingDelegation{Entries: entries} } -func createRedelegation(tokensPerEntry []int64) stakingtypes.Redelegation { +// createRedelegation creates a redelegation with `len(initialBalances)` entries +func createRedelegation(initialBalances []int64, completionTimes []time.Time) stakingtypes.Redelegation { var entries []stakingtypes.RedelegationEntry - for _, t := range tokensPerEntry { + for i, balance := range initialBalances { entry := stakingtypes.RedelegationEntry{ - InitialBalance: sdk.NewInt(t), + InitialBalance: sdk.NewInt(balance), + CompletionTime: completionTimes[i], } entries = append(entries, entry) } @@ -166,10 +169,16 @@ func createRedelegation(tokensPerEntry []int64) stakingtypes.Redelegation { return stakingtypes.Redelegation{Entries: entries} } +// TestComputePowerToSlash tests that `ComputePowerToSlash` computes the correct power to be slashed based on +// the tokens in non-mature undelegation and redelegation entries, as well as the current power of the validator func TestComputePowerToSlash(t *testing.T) { - providerKeeper, _, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + now := ctx.BlockHeader().Time + nowPlus1Hour := now.Add(time.Hour) + nowMinus1Hour := now.Add(-time.Hour) + testCases := []struct { name string undelegations []stakingtypes.UnbondingDelegation @@ -182,12 +191,12 @@ func TestComputePowerToSlash(t *testing.T) { "both undelegations and redelegations 1", // 1000 total undelegation tokens []stakingtypes.UnbondingDelegation{ - createUndelegation([]int64{250, 250}), - createUndelegation([]int64{500})}, + createUndelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, nowPlus1Hour}), + createUndelegation([]int64{500}, []time.Time{nowPlus1Hour, nowPlus1Hour})}, // 1000 total redelegation tokens []stakingtypes.Redelegation{ - createRedelegation([]int64{500}), - createRedelegation([]int64{250, 250}), + createRedelegation([]int64{500}, []time.Time{nowPlus1Hour, nowPlus1Hour}), + createRedelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, nowPlus1Hour}), }, int64(1000), sdk.NewInt(1), @@ -197,19 +206,19 @@ func TestComputePowerToSlash(t *testing.T) { "both undelegations and redelegations 2", // 2000 total undelegation tokens []stakingtypes.UnbondingDelegation{ - createUndelegation([]int64{250, 250}), - createUndelegation([]int64{}), - createUndelegation([]int64{100, 100}), - createUndelegation([]int64{800}), - createUndelegation([]int64{500})}, + createUndelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, nowPlus1Hour}), + createUndelegation([]int64{}, []time.Time{}), + createUndelegation([]int64{100, 100}, []time.Time{nowPlus1Hour, nowPlus1Hour}), + createUndelegation([]int64{800}, []time.Time{nowPlus1Hour}), + createUndelegation([]int64{500}, []time.Time{nowPlus1Hour})}, // 3500 total redelegation tokens []stakingtypes.Redelegation{ - createRedelegation([]int64{}), - createRedelegation([]int64{1600}), - createRedelegation([]int64{350, 250}), - createRedelegation([]int64{700, 200}), - createRedelegation([]int64{}), - createRedelegation([]int64{400}), + createRedelegation([]int64{}, []time.Time{}), + createRedelegation([]int64{1600}, []time.Time{nowPlus1Hour}), + createRedelegation([]int64{350, 250}, []time.Time{nowPlus1Hour, nowPlus1Hour}), + createRedelegation([]int64{700, 200}, []time.Time{nowPlus1Hour, nowPlus1Hour}), + createRedelegation([]int64{}, []time.Time{}), + createRedelegation([]int64{400}, []time.Time{nowPlus1Hour}), }, int64(8391), sdk.NewInt(2), @@ -228,12 +237,12 @@ func TestComputePowerToSlash(t *testing.T) { []stakingtypes.UnbondingDelegation{}, // 2000 total redelegation tokens []stakingtypes.Redelegation{ - createRedelegation([]int64{}), - createRedelegation([]int64{500}), - createRedelegation([]int64{250, 250}), - createRedelegation([]int64{700, 200}), - createRedelegation([]int64{}), - createRedelegation([]int64{100}), + createRedelegation([]int64{}, []time.Time{}), + createRedelegation([]int64{500}, []time.Time{nowPlus1Hour}), + createRedelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, nowPlus1Hour}), + createRedelegation([]int64{700, 200}, []time.Time{nowPlus1Hour, nowPlus1Hour}), + createRedelegation([]int64{}, []time.Time{}), + createRedelegation([]int64{100}, []time.Time{nowPlus1Hour}), }, int64(17), sdk.NewInt(3), @@ -243,20 +252,46 @@ func TestComputePowerToSlash(t *testing.T) { "no redelegations", // 2000 total undelegation tokens []stakingtypes.UnbondingDelegation{ - createUndelegation([]int64{250, 250}), - createUndelegation([]int64{}), - createUndelegation([]int64{100, 100}), - createUndelegation([]int64{800}), - createUndelegation([]int64{500})}, + createUndelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, nowPlus1Hour}), + createUndelegation([]int64{}, []time.Time{}), + createUndelegation([]int64{100, 100}, []time.Time{nowPlus1Hour, nowPlus1Hour}), + createUndelegation([]int64{800}, []time.Time{nowPlus1Hour}), + createUndelegation([]int64{500}, []time.Time{nowPlus1Hour})}, []stakingtypes.Redelegation{}, int64(1), sdk.NewInt(3), int64(2000/3 + 1), }, + { + "both (mature) undelegations and redelegations", + // 2000 total undelegation tokens, 250 + 100 + 500 = 850 of those are from mature undelegations, + // so 2000 - 850 = 1150 + []stakingtypes.UnbondingDelegation{ + createUndelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, nowMinus1Hour}), + createUndelegation([]int64{}, []time.Time{}), + createUndelegation([]int64{100, 100}, []time.Time{nowMinus1Hour, nowPlus1Hour}), + createUndelegation([]int64{800}, []time.Time{nowPlus1Hour}), + createUndelegation([]int64{500}, []time.Time{nowMinus1Hour})}, + // 3500 total redelegation tokens, 350 + 200 + 400 = 950 of those are from mature redelegations + // so 3500 - 950 = 2550 + []stakingtypes.Redelegation{ + createRedelegation([]int64{}, []time.Time{}), + createRedelegation([]int64{1600}, []time.Time{nowPlus1Hour}), + createRedelegation([]int64{350, 250}, []time.Time{nowMinus1Hour, nowPlus1Hour}), + createRedelegation([]int64{700, 200}, []time.Time{nowPlus1Hour, nowMinus1Hour}), + createRedelegation([]int64{}, []time.Time{}), + createRedelegation([]int64{400}, []time.Time{nowMinus1Hour}), + }, + int64(8391), + sdk.NewInt(2), + int64((1150+2550)/2 + 8391), + }, } for _, tc := range testCases { - actualPower := providerKeeper.ComputePowerToSlash(tc.undelegations, tc.redelegations, tc.power, tc.powerReduction) + actualPower := providerKeeper.ComputePowerToSlash(now, + tc.undelegations, tc.redelegations, tc.power, tc.powerReduction) + if tc.expectedPower != actualPower { require.Fail(t, fmt.Sprintf("\"%s\" failed", tc.name), "expected is %d but actual is %d", tc.expectedPower, actualPower) @@ -264,16 +299,16 @@ func TestComputePowerToSlash(t *testing.T) { } } -// FIXME: Probably the following test could be a combination with computePowertoSlash ... -// Not sure I like this -// the actual slashing should be checked from integration tests // TestSlashValidator asserts that `SlashValidator` calls the staking module's `Slash` method // with the correct arguments (i.e., `infractionHeight` of 0 and the expected slash power) func TestSlashValidator(t *testing.T) { keeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() - ctx = ctx.WithBlockTime(time.Now()) + now := ctx.BlockHeader().Time + nowPlus1Hour := now.Add(time.Hour) + nowMinus1Hour := now.Add(-time.Hour) + keeperParams := testkeeper.NewInMemKeeperParams(t) testkeeper.NewInMemProviderKeeper(keeperParams, mocks) @@ -283,13 +318,14 @@ func TestSlashValidator(t *testing.T) { consAddr, _ := validator.GetConsAddr() providerAddr := types.NewProviderConsAddress(consAddr) - // we create 1000 tokens worth of undelegations and 1000 tokens worth of redelegations + // we create 1000 tokens worth of undelegations, 750 of them are non-matured + // we also create 1000 tokens worth of redelegations, 750 of them are non-matured undelegations := []stakingtypes.UnbondingDelegation{ - createUndelegation([]int64{250, 250}), - createUndelegation([]int64{500})} + createUndelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, nowMinus1Hour}), + createUndelegation([]int64{500}, []time.Time{nowPlus1Hour})} redelegations := []stakingtypes.Redelegation{ - createRedelegation([]int64{250, 250}), - createRedelegation([]int64{500})} + createRedelegation([]int64{250, 250}, []time.Time{nowMinus1Hour, nowPlus1Hour}), + createRedelegation([]int64{500}, []time.Time{nowPlus1Hour})} // validator's current power currentPower := int64(3000) @@ -298,9 +334,9 @@ func TestSlashValidator(t *testing.T) { slashFraction, _ := sdk.NewDecFromStr("0.5") // the call to `Slash` should provide an `infractionHeight` of 0 and an expected power of - // (1000 (undelegations) + 1000 (redelegations)) / 2 (= powerReduction) + 3000 (currentPower) = 4000 + // (750 (undelegations) + 750 (redelegations)) / 2 (= powerReduction) + 3000 (currentPower) = 3750 expectedInfractionHeight := int64(0) - expectedSlashPower := int64(4000) + expectedSlashPower := int64(3750) expectedCalls := []*gomock.Call{ mocks.MockStakingKeeper.EXPECT(). diff --git a/x/ccv/types/expected_keepers.go b/x/ccv/types/expected_keepers.go index 6cf132e5d4..bc55ebbf7e 100644 --- a/x/ccv/types/expected_keepers.go +++ b/x/ccv/types/expected_keepers.go @@ -2,7 +2,6 @@ package types import ( context "context" - "github.com/cosmos/cosmos-sdk/x/evidence/exported" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -51,7 +50,6 @@ type StakingKeeper interface { type EvidenceKeeper interface { HandleEquivocationEvidence(ctx sdk.Context, evidence *evidencetypes.Equivocation) - SetEvidence(ctx sdk.Context, evidence exported.Evidence) } // SlashingKeeper defines the contract expected to perform ccv slashing From 9ba3a3db86fe89d7043883d475bc30c7ceb1fad6 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Tue, 12 Sep 2023 15:50:06 +0200 Subject: [PATCH 10/29] clean up --- x/ccv/provider/keeper/punish_validator.go | 4 ++-- .../provider/keeper/punish_validator_test.go | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index b0ae53deda..db70da712a 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -31,7 +31,7 @@ func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.Pr } // Jail the validator to trigger the unbonding of the validator - // (see cosmos/cosmos-sdk/blob/v0.47.0/x/staking/keeper/val_state_change.go#L194). + // (see cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/x/staking/keeper/val_state_change.go#L192). k.slashingKeeper.JailUntil(ctx, providerAddr.ToSdkConsAddr(), evidencetypes.DoubleSignJailEndTime) // Tombstone the validator so that we cannot slash the validator more than once @@ -79,7 +79,7 @@ func (k Keeper) ComputePowerToSlash(now time.Time, undelegations []stakingtypes. return power + undelegationsAndRedelegationsInPower } -// Slash validator based on the `providerAddr` +// SlashValidator slashes validator with `providerAddr` func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) { logger := k.Logger(ctx) diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index ec5ba0d8ab..d67dede2e8 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -175,9 +175,10 @@ func TestComputePowerToSlash(t *testing.T) { providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + // undelegation or redelegation entries with completion time `now` have matured now := ctx.BlockHeader().Time + // undelegation or redelegation entries with completion time one hour in the future have not yet matured nowPlus1Hour := now.Add(time.Hour) - nowMinus1Hour := now.Add(-time.Hour) testCases := []struct { name string @@ -267,20 +268,20 @@ func TestComputePowerToSlash(t *testing.T) { // 2000 total undelegation tokens, 250 + 100 + 500 = 850 of those are from mature undelegations, // so 2000 - 850 = 1150 []stakingtypes.UnbondingDelegation{ - createUndelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, nowMinus1Hour}), + createUndelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, now}), createUndelegation([]int64{}, []time.Time{}), - createUndelegation([]int64{100, 100}, []time.Time{nowMinus1Hour, nowPlus1Hour}), + createUndelegation([]int64{100, 100}, []time.Time{now, nowPlus1Hour}), createUndelegation([]int64{800}, []time.Time{nowPlus1Hour}), - createUndelegation([]int64{500}, []time.Time{nowMinus1Hour})}, + createUndelegation([]int64{500}, []time.Time{now})}, // 3500 total redelegation tokens, 350 + 200 + 400 = 950 of those are from mature redelegations // so 3500 - 950 = 2550 []stakingtypes.Redelegation{ createRedelegation([]int64{}, []time.Time{}), createRedelegation([]int64{1600}, []time.Time{nowPlus1Hour}), - createRedelegation([]int64{350, 250}, []time.Time{nowMinus1Hour, nowPlus1Hour}), - createRedelegation([]int64{700, 200}, []time.Time{nowPlus1Hour, nowMinus1Hour}), + createRedelegation([]int64{350, 250}, []time.Time{now, nowPlus1Hour}), + createRedelegation([]int64{700, 200}, []time.Time{nowPlus1Hour, now}), createRedelegation([]int64{}, []time.Time{}), - createRedelegation([]int64{400}, []time.Time{nowMinus1Hour}), + createRedelegation([]int64{400}, []time.Time{now}), }, int64(8391), sdk.NewInt(2), @@ -305,9 +306,10 @@ func TestSlashValidator(t *testing.T) { keeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + // undelegation or redelegation entries with completion time `now` have matured now := ctx.BlockHeader().Time + // undelegation or redelegation entries with completion time one hour in the future have not yet matured nowPlus1Hour := now.Add(time.Hour) - nowMinus1Hour := now.Add(-time.Hour) keeperParams := testkeeper.NewInMemKeeperParams(t) testkeeper.NewInMemProviderKeeper(keeperParams, mocks) @@ -321,10 +323,10 @@ func TestSlashValidator(t *testing.T) { // we create 1000 tokens worth of undelegations, 750 of them are non-matured // we also create 1000 tokens worth of redelegations, 750 of them are non-matured undelegations := []stakingtypes.UnbondingDelegation{ - createUndelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, nowMinus1Hour}), + createUndelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, now}), createUndelegation([]int64{500}, []time.Time{nowPlus1Hour})} redelegations := []stakingtypes.Redelegation{ - createRedelegation([]int64{250, 250}, []time.Time{nowMinus1Hour, nowPlus1Hour}), + createRedelegation([]int64{250, 250}, []time.Time{now, nowPlus1Hour}), createRedelegation([]int64{500}, []time.Time{nowPlus1Hour})} // validator's current power From b1a6f3101043982debeb04c79392b9beaa811ac8 Mon Sep 17 00:00:00 2001 From: Marius Poke Date: Thu, 14 Sep 2023 15:37:11 +0200 Subject: [PATCH 11/29] feat!: Cryptographic verification of equivocation (#1287) * 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 * feat: improve ICS misbehaviour E2E testing coverage (#1225) * update e2e tests * update the chain halt assertion * refactor: address comments of ICS Misbehaviour PRs #826 and #1148 (#1223) * remove interface * improve comment * update godoc * address last comments * 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 * fix: tiny bug in `NewSubmitConsumerDoubleVotingCmd` (#1247) * fix double voting cli * fix bug double signing handler * godoc * nits * revert wrong push of lasts commits * fix: make `HandleConsumerDoubleVoting` works with provider pubkeys (#1254) * fix double voting cli * fix bug double signing handler * godoc * nits * lint * nit * fix: verify equivocation using validator pubkey in `SubmitConsumerDoubleVoting` msg (#1264) * verify dv evidence using malicious validator pubkey in infraction block header * nits * nits * 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 * 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> * save * fix nits * update changelog and fix nits --------- Co-authored-by: Simon Noetzlin Co-authored-by: Anca Zamfir Co-authored-by: Philip Offtermatt <57488781+p-offtermatt@users.noreply.github.com> --- CHANGELOG.md | 7 + Dockerfile | 2 +- docs/docs/features/slashing.md | 16 + .../ccv/provider/v1/tx.proto | 35 + tests/e2e/actions.go | 52 +- tests/e2e/actions_consumer_misbehaviour.go | 119 +++ tests/e2e/config.go | 84 ++ tests/e2e/main.go | 11 + tests/e2e/state.go | 87 ++ tests/e2e/steps.go | 15 + tests/e2e/steps_consumer_misbehaviour.go | 277 ++++++ tests/e2e/steps_double_sign.go | 76 ++ tests/e2e/testnet-scripts/fork-consumer.sh | 114 +++ tests/e2e/testnet-scripts/hermes-config.toml | 18 +- tests/e2e/testnet-scripts/start-chain.sh | 9 +- tests/integration/double_vote.go | 198 ++++ tests/integration/misbehaviour.go | 393 ++++++++ testutil/crypto/evidence.go | 56 ++ testutil/integration/debug_test.go | 24 + testutil/keeper/mocks.go | 55 ++ .../proto/tendermint/types/evidence.proto | 38 + x/ccv/provider/client/cli/tx.go | 108 +++ x/ccv/provider/client/proposal_handler.go | 2 +- x/ccv/provider/handler.go | 6 + x/ccv/provider/keeper/double_vote.go | 104 ++ x/ccv/provider/keeper/double_vote_test.go | 284 ++++++ x/ccv/provider/keeper/misbehaviour.go | 143 +++ x/ccv/provider/keeper/msg_server.go | 73 +- x/ccv/provider/keeper/punish_validator.go | 38 + .../provider/keeper/punish_validator_test.go | 132 +++ x/ccv/provider/types/codec.go | 10 + x/ccv/provider/types/msg.go | 110 ++- x/ccv/provider/types/tx.pb.go | 890 +++++++++++++++++- x/ccv/types/errors.go | 39 +- x/ccv/types/events.go | 33 +- x/ccv/types/expected_keepers.go | 4 + 36 files changed, 3581 insertions(+), 81 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/double_vote.go create mode 100644 tests/integration/misbehaviour.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/misbehaviour.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/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/Dockerfile b/Dockerfile index 4d81392316..7f58c49632 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:evidence-cmd AS hermes-builder # Get CometMock FROM informalofftermatt/cometmock:latest as cometmock-builder 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/proto/interchain_security/ccv/provider/v1/tx.proto b/proto/interchain_security/ccv/provider/v1/tx.proto index 528065180a..519118bb68 100644 --- a/proto/interchain_security/ccv/provider/v1/tx.proto +++ b/proto/interchain_security/ccv/provider/v1/tx.proto @@ -7,10 +7,15 @@ 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"; +import "tendermint/types/evidence.proto"; + // Msg defines the Msg service. service Msg { rpc AssignConsumerKey(MsgAssignConsumerKey) returns (MsgAssignConsumerKeyResponse); + rpc SubmitConsumerMisbehaviour(MsgSubmitConsumerMisbehaviour) returns (MsgSubmitConsumerMisbehaviourResponse); + rpc SubmitConsumerDoubleVoting(MsgSubmitConsumerDoubleVoting) returns (MsgSubmitConsumerDoubleVotingResponse); } message MsgAssignConsumerKey { @@ -28,3 +33,33 @@ message MsgAssignConsumerKey { } message MsgAssignConsumerKeyResponse {} + + +// MsgSubmitConsumerMisbehaviour defines a message that reports a light client attack, +// also known as a misbehaviour, observed on a consumer chain +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 {} + + +// 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; + 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 {} diff --git a/tests/e2e/actions.go b/tests/e2e/actions.go index 77b56b6cbe..4419d1dcff 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 { @@ -565,7 +568,7 @@ func (tr TestRun) startConsumerChain( chain: action.consumerChain, validators: action.validators, genesisChanges: consumerGenesis, - skipGentx: true, + isConsumer: true, }, verbose) } @@ -699,6 +702,7 @@ func (tr TestRun) startChangeover( type addChainToRelayerAction struct { chain chainID validator validatorID + consumer bool } const hermesChainConfigTemplate = ` @@ -715,7 +719,8 @@ 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] denom = "stake" @@ -814,7 +819,7 @@ func (tr TestRun) addChainToHermes( keyName, rpcAddr, wsAddr, - // action.consumer, + action.consumer, ) bashCommand := fmt.Sprintf(`echo '%s' >> %s`, chainConfig, "/root/.hermes/config.toml") @@ -828,7 +833,15 @@ 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) //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. bz, err = exec.Command("docker", "exec", tr.containerConfig.instanceName, "bash", "-c", saveMnemonicCommand, @@ -1716,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), @@ -1864,6 +1877,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 @@ -1925,3 +1940,26 @@ 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 startConsumerEvidenceDetectorAction struct { + chain chainID +} + +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() + 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 new file mode 100644 index 0000000000..b5c9efbc2a --- /dev/null +++ b/tests/e2e/actions_consumer_misbehaviour.go @@ -0,0 +1,119 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os/exec" + "strconv" + "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 + 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 { + log.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 { + log.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 { + chain chainID + hostChain chainID + relayerConfig string + clientID string +} + +func (tr TestRun) updateLightClient( + action updateLightClientAction, + verbose bool, +) { + // 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, + "update", + "client", + "--client", action.clientID, + "--host-chain", string(action.hostChain), + "--trusted-height", strconv.Itoa(int(trustedHeight.RevisionHeight)), + ) + 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) +} + +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/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 6e6c3b6cac..2ec2c2da9d 100644 --- a/tests/e2e/main.go +++ b/tests/e2e/main.go @@ -62,7 +62,10 @@ func main() { {DemocracyTestRun(true), democracySteps}, {DemocracyTestRun(false), rewardDenomConsumerSteps}, {SlashThrottleTestRun(), slashThrottleSteps}, + {ConsumerMisbehaviourTestRun(), consumerMisbehaviourSteps}, + {DefaultTestRun(), consumerDoubleSignSteps}, } + if includeMultiConsumer != nil && *includeMultiConsumer { testRuns = append(testRuns, testRunWithSteps{MultiConsumerTestRun(), multipleConsumers}) } @@ -173,6 +176,14 @@ func (tr *TestRun) runStep(step Step, verbose bool) { tr.waitForSlashThrottleDequeue(action, verbose) case startRelayerAction: tr.startRelayer(action, verbose) + case forkConsumerChainAction: + tr.forkConsumerChain(action, verbose) + case updateLightClientAction: + tr.updateLightClient(action, verbose) + case assertChainIsHaltedAction: + tr.assertChainIsHalted(action, verbose) + case startConsumerEvidenceDetectorAction: + tr.startConsumerEvidenceDetector(action, verbose) case submitChangeRewardDenomsProposalAction: tr.submitChangeRewardDenomsProposal(action, verbose) default: diff --git a/tests/e2e/state.go b/tests/e2e/state.go index 15500dd01f..d0d908a494 100644 --- a/tests/e2e/state.go +++ b/tests/e2e/state.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "fmt" "log" "os/exec" @@ -28,6 +29,7 @@ type ChainState struct { ConsumerChainQueueSizes *map[chainID]uint GlobalSlashQueueSize *uint RegisteredConsumerRewardDenoms *[]string + ClientsFrozenHeights *map[string]clienttypes.Height } type Proposal interface { @@ -184,6 +186,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 +747,80 @@ 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)} +} + +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.go b/tests/e2e/steps.go index c9830c2aa2..1183e0bda5 100644 --- a/tests/e2e/steps.go +++ b/tests/e2e/steps.go @@ -88,3 +88,18 @@ var changeoverSteps = concatSteps( stepsPostChangeoverDelegate("sover"), ) + +var consumerMisbehaviourSteps = concatSteps( + // start provider and consumer chain + stepsStartChainsWithSoftOptOut("consu"), + // 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_consumer_misbehaviour.go b/tests/e2e/steps_consumer_misbehaviour.go new file mode 100644 index 0000000000..92402d6dc6 --- /dev/null +++ b/tests/e2e/steps_consumer_misbehaviour.go @@ -0,0 +1,277 @@ +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 the alice validator node + action: forkConsumerChainAction{ + consumerChain: chainID(consumerName), + providerChain: chainID("provi"), + validator: validatorID("alice"), + relayerConfig: forkRelayerConfig, + }, + state: State{}, + }, + // start relayer to detect IBC misbehaviour + { + action: startRelayerAction{}, + state: State{}, + }, + // run Hermes relayer instance to detect the ICS misbehaviour + // and jail alice on the provider + { + action: startConsumerEvidenceDetectorAction{ + 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 + }, + state: State{ + chainID("provi"): ChainState{ + // alice should be jailed on the provider + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 0, + validatorID("bob"): 20, + }, + // The consumer light client should be frozen on the provider + ClientsFrozenHeights: &map[string]clienttypes.Height{ + consumerClientID: { + RevisionNumber: 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, + }, + }, + }, + }, + } +} diff --git a/tests/e2e/steps_double_sign.go b/tests/e2e/steps_double_sign.go index c007fa5c1c..2adcac14cb 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: startConsumerEvidenceDetectorAction{ + 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, + }, + }, + }, + }, + } +} diff --git a/tests/e2e/testnet-scripts/fork-consumer.sh b/tests/e2e/testnet-scripts/fork-consumer.sh new file mode 100644 index 0000000000..7c12438b71 --- /dev/null +++ b/tests/e2e/testnet-scripts/fork-consumer.sh @@ -0,0 +1,114 @@ +#!/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/double_vote.go b/tests/integration/double_vote.go new file mode 100644 index 0000000000..c79b92115e --- /dev/null +++ b/tests/integration/double_vote.go @@ -0,0 +1,198 @@ +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" +) + +// 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) + } + + 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()] + + 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 + + // 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(), + provValSet, + provSigner, + s.consumerChain.ChainID, + ) + + provBadVote := testutil.MakeAndSignVote( + blockID2, + s.consumerCtx().BlockHeight(), + s.consumerCtx().BlockTime(), + provValSet, + provSigner, + s.consumerChain.ChainID, + ) + + testCases := []struct { + name string + ev *tmtypes.DuplicateVoteEvidence + chainID string + pubkey crypto.PubKey + expPass bool + }{ + { + "invalid consumer chain id - shouldn't pass", + &tmtypes.DuplicateVoteEvidence{ + VoteA: consuVote, + VoteB: consuBadVote, + ValidatorPower: consuVal.VotingPower, + TotalVotingPower: consuVal.VotingPower, + 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, + }, + { + // create an invalid evidence containing two identical votes + "invalid double voting evidence with identical votes - shouldn't pass", + &tmtypes.DuplicateVoteEvidence{ + VoteA: consuVote, + VoteB: consuVote, + ValidatorPower: consuVal.VotingPower, + TotalVotingPower: consuVal.VotingPower, + Timestamp: s.consumerCtx().BlockTime(), + }, + s.consumerChain.ChainID, + consuVal.PubKey, + 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 1 - should pass", + &tmtypes.DuplicateVoteEvidence{ + VoteA: consuVote, + VoteB: consuBadVote, + ValidatorPower: consuVal.VotingPower, + TotalVotingPower: consuVal.VotingPower, + Timestamp: s.consumerCtx().BlockTime(), + }, + s.consumerChain.ChainID, + consuVal.PubKey, + 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, + provVal.PubKey, + true, + }, + } + + 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) + } + + // 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 { + s.Require().NoError(err) + + // verifies that the jailing has occurred + 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(provCtx, provAddr.ToSdkConsAddr())) + } + }) + } +} diff --git a/tests/integration/misbehaviour.go b/tests/integration/misbehaviour.go new file mode 100644 index 0000000000..f7cebc84c0 --- /dev/null +++ b/tests/integration/misbehaviour.go @@ -0,0 +1,393 @@ +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 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) + } +} + +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/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 b6456663a9..fcb19d5255 100644 --- a/testutil/integration/debug_test.go +++ b/testutil/integration/debug_test.go @@ -256,3 +256,27 @@ func TestQueueAndSendVSCMaturedPackets(t *testing.T) { func TestRecycleTransferChannel(t *testing.T) { runCCVTestByName(t, "TestRecycleTransferChannel") } + +// +// Misbehaviour tests +// + +func TestHandleConsumerMisbehaviour(t *testing.T) { + runCCVTestByName(t, "TestHandleConsumerMisbehaviour") +} + +func TestGetByzantineValidators(t *testing.T) { + runCCVTestByName(t, "TestGetByzantineValidators") +} + +func TestCheckMisbehaviour(t *testing.T) { + runCCVTestByName(t, "TestCheckMisbehaviour") +} + +// +// Equivocation test +// + +func TestHandleConsumerDoubleVoting(t *testing.T) { + runCCVTestByName(t, "TestHandleConsumerDoubleVoting") +} diff --git a/testutil/keeper/mocks.go b/testutil/keeper/mocks.go index 9976a03b15..c005424de5 100644 --- a/testutil/keeper/mocks.go +++ b/testutil/keeper/mocks.go @@ -693,6 +693,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() @@ -708,6 +736,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() @@ -790,6 +833,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/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 724e388e84..f32bc304df 100644 --- a/x/ccv/provider/client/cli/tx.go +++ b/x/ccv/provider/client/cli/tx.go @@ -2,12 +2,17 @@ package cli import ( "fmt" + "strings" "github.com/spf13/cobra" "github.com/cosmos/cosmos-sdk/client" "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" + 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" ) @@ -23,6 +28,8 @@ func GetTxCmd() *cobra.Command { } cmd.AddCommand(NewAssignConsumerKeyCmd()) + cmd.AddCommand(NewSubmitConsumerMisbehaviourCmd()) + cmd.AddCommand(NewSubmitConsumerDoubleVotingCmd()) return cmd } @@ -61,3 +68,104 @@ func NewAssignConsumerKeyCmd() *cobra.Command { 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 +} + +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", + Long: strings.TrimSpace( + 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] [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 { + 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, &header) + 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/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: { diff --git a/x/ccv/provider/handler.go b/x/ccv/provider/handler.go index 512cbf9afd..b67f3fbb31 100644 --- a/x/ccv/provider/handler.go +++ b/x/ccv/provider/handler.go @@ -18,6 +18,12 @@ func NewHandler(k *keeper.Keeper) sdk.Handler { case *types.MsgAssignConsumerKey: res, err := msgServer.AssignConsumerKey(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) + 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..ccca1967fb --- /dev/null +++ b/x/ccv/provider/keeper/double_vote.go @@ -0,0 +1,104 @@ +package keeper + +import ( + "bytes" + "fmt" + + 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" + tmtypes "github.com/tendermint/tendermint/types" +) + +// 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, + chainID, + types.NewConsumerConsAddress(sdk.ConsAddress(evidence.VoteA.ValidatorAddress.Bytes())), + ) + + // execute the jailing + k.JailValidator(ctx, providerAddr) + + k.Logger(ctx).Info( + "confirmed equivocation", + "byzantine validator address", providerAddr.String(), + ) + + 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 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. + + // 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.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) + } + + // Addresses must be the same + if !bytes.Equal(evidence.VoteA.ValidatorAddress, evidence.VoteB.ValidatorAddress) { + return sdkerrors.Wrapf( + ccvtypes.ErrInvalidDoubleVotingEvidence, + "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.ErrInvalidDoubleVotingEvidence, + "block IDs are the same (%v) - not a real duplicate vote", + evidence.VoteA.BlockID, + ) + } + + va := evidence.VoteA.ToProto() + vb := evidence.VoteB.ToProto() + + // signatures must be valid + if !pubkey.VerifySignature(tmtypes.VoteSignBytes(chainID, va), evidence.VoteA.Signature) { + return fmt.Errorf("verifying VoteA: %w", tmtypes.ErrVoteInvalidSignature) + } + if !pubkey.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..cc8280b14d --- /dev/null +++ b/x/ccv/provider/keeper/double_vote_test.go @@ -0,0 +1,284 @@ +package keeper_test + +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" + 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 := cryptocodec.FromTmPubKeyInterface(val1.PubKey) + require.NoError(t, err) + + valPubkey2, err := cryptocodec.FromTmPubKeyInterface(val2.PubKey) + require.NoError(t, err) + + testCases := []struct { + name string + votes []*tmtypes.Vote + chainID string + pubkey cryptotypes.PubKey + 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, + nil, + 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 new file mode 100644 index 0000000000..f149470118 --- /dev/null +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -0,0 +1,143 @@ +package keeper + +import ( + "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" + 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 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 + } + + provAddrs := make([]types.ProviderConsAddress, len(byzantineValidators)) + + // jail the Byzantine validators + for _, v := range byzantineValidators { + 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", provAddrs, + ) + + 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) + } + } + + 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 { + 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 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 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 + } + + return nil +} diff --git a/x/ccv/provider/keeper/msg_server.go b/x/ccv/provider/keeper/msg_server.go index 58c621d74b..a24d6ef919 100644 --- a/x/ccv/provider/keeper/msg_server.go +++ b/x/ccv/provider/keeper/msg_server.go @@ -4,12 +4,15 @@ 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" "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 { @@ -24,7 +27,6 @@ func NewMsgServerImpl(keeper *Keeper) types.MsgServer { var _ types.MsgServer = msgServer{} -// CreateValidator defines a method for creating a new validator func (k msgServer) AssignConsumerKey(goCtx context.Context, msg *types.MsgAssignConsumerKey) (*types.MsgAssignConsumerKeyResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) @@ -105,3 +107,72 @@ func (k msgServer) AssignConsumerKey(goCtx context.Context, msg *types.MsgAssign return &types.MsgAssignConsumerKeyResponse{}, 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 nil, 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 +} + +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 + } + + // 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.ErrInvalidDoubleVotingEvidence, + "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{ + sdk.NewEvent( + ccvtypes.EventTypeSubmitConsumerDoubleVoting, + 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/codec.go b/x/ccv/provider/types/codec.go index aacc63731b..919386a1b4 100644 --- a/x/ccv/provider/types/codec.go +++ b/x/ccv/provider/types/codec.go @@ -36,6 +36,16 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) { &ChangeRewardDenomsProposal{}, ) + registry.RegisterImplementations( + (*sdk.Msg)(nil), + &MsgSubmitConsumerMisbehaviour{}, + ) + + registry.RegisterImplementations( + (*sdk.Msg)(nil), + &MsgSubmitConsumerDoubleVoting{}, + ) + msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) } diff --git a/x/ccv/provider/types/msg.go b/x/ccv/provider/types/msg.go index 901aa03600..3748d1cb6a 100644 --- a/x/ccv/provider/types/msg.go +++ b/x/ccv/provider/types/msg.go @@ -2,17 +2,27 @@ 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 const ( - TypeMsgAssignConsumerKey = "assign_consumer_key" + TypeMsgAssignConsumerKey = "assign_consumer_key" + TypeMsgSubmitConsumerMisbehaviour = "submit_consumer_misbehaviour" + TypeMsgSubmitConsumerDoubleVoting = "submit_consumer_double_vote" ) -var _ sdk.Msg = &MsgAssignConsumerKey{} +var ( + _ sdk.Msg = &MsgAssignConsumerKey{} + _ sdk.Msg = &MsgSubmitConsumerMisbehaviour{} + _ sdk.Msg = &MsgSubmitConsumerDoubleVoting{} +) // NewMsgAssignConsumerKey creates a new MsgAssignConsumerKey instance. // Delegator address and validator address are the same. @@ -93,3 +103,99 @@ func ParseConsumerKeyFromJson(jsonStr string) (pkType, key string, err error) { } return pubKey.Type, pubKey.Key, 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} +} + +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 == 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") + } + + if msg.InfractionBlockHeader.ValidatorSet == nil { + return fmt.Errorf("invalid infraction block header, validator set is 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 40314e1c41..b4e78f90d4 100644 --- a/x/ccv/provider/types/tx.pb.go +++ b/x/ccv/provider/types/tx.pb.go @@ -7,10 +7,12 @@ 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" _ "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" @@ -111,9 +113,172 @@ func (m *MsgAssignConsumerKeyResponse) XXX_DiscardUnknown() { var xxx_messageInfo_MsgAssignConsumerKeyResponse proto.InternalMessageInfo +// MsgSubmitConsumerMisbehaviour defines a message that reports a light client attack, +// +// also known as a misbehaviour, observed on a consumer chain +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{2} +} +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{3} +} +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 + +// MsgSubmitConsumerDoubleVoting defines a message that reports +// a double signing infraction 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{4} +} +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{5} +} +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") + 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() { @@ -121,31 +286,46 @@ func init() { } var fileDescriptor_43221a4391e9fbf4 = []byte{ - // 375 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x52, 0x3d, 0x4f, 0xeb, 0x30, - 0x14, 0x8d, 0x5f, 0xa5, 0xf7, 0xfa, 0xfc, 0xfa, 0x9e, 0xf4, 0xa2, 0x0e, 0x6d, 0x55, 0xa5, 0x10, - 0x16, 0x06, 0x88, 0xd5, 0x32, 0x20, 0x2a, 0x31, 0xb4, 0x4c, 0x08, 0x75, 0xe9, 0x82, 0xc4, 0x12, - 0xa5, 0x8e, 0x71, 0x2d, 0x1a, 0x3b, 0xb2, 0x9d, 0xa8, 0xf9, 0x07, 0x8c, 0x30, 0x21, 0xb6, 0xfe, - 0x1c, 0xc6, 0x8e, 0x4c, 0x08, 0xb5, 0x0b, 0x33, 0xbf, 0x00, 0x35, 0x1f, 0x54, 0x88, 0x0e, 0x88, - 0xed, 0xde, 0x7b, 0x8e, 0xcf, 0x39, 0xf2, 0xbd, 0x70, 0x8f, 0x71, 0x4d, 0x24, 0x1e, 0x7b, 0x8c, - 0xbb, 0x8a, 0xe0, 0x48, 0x32, 0x9d, 0x20, 0x8c, 0x63, 0x14, 0x4a, 0x11, 0x33, 0x9f, 0x48, 0x14, - 0xb7, 0x91, 0x9e, 0x3a, 0xa1, 0x14, 0x5a, 0x98, 0x3b, 0x1b, 0xd8, 0x0e, 0xc6, 0xb1, 0x53, 0xb0, - 0x9d, 0xb8, 0xdd, 0x68, 0x52, 0x21, 0xe8, 0x84, 0x20, 0x2f, 0x64, 0xc8, 0xe3, 0x5c, 0x68, 0x4f, - 0x33, 0xc1, 0x55, 0x26, 0xd1, 0xa8, 0x52, 0x41, 0x45, 0x5a, 0xa2, 0x55, 0x95, 0x4f, 0xeb, 0x58, - 0xa8, 0x40, 0x28, 0x37, 0x03, 0xb2, 0xa6, 0x80, 0x72, 0xb9, 0xb4, 0x1b, 0x45, 0x97, 0xc8, 0xe3, - 0x49, 0x06, 0xd9, 0x77, 0x00, 0x56, 0x07, 0x8a, 0xf6, 0x94, 0x62, 0x94, 0x9f, 0x08, 0xae, 0xa2, - 0x80, 0xc8, 0x33, 0x92, 0x98, 0x75, 0x58, 0xce, 0x42, 0x32, 0xbf, 0x06, 0xb6, 0xc0, 0xee, 0xef, - 0xe1, 0xaf, 0xb4, 0x3f, 0xf5, 0xcd, 0x43, 0xf8, 0xb7, 0x08, 0xeb, 0x7a, 0xbe, 0x2f, 0x6b, 0x3f, - 0x56, 0x78, 0xdf, 0x7c, 0x7d, 0x6a, 0xfd, 0x4b, 0xbc, 0x60, 0xd2, 0xb5, 0x57, 0x53, 0xa2, 0x94, - 0x3d, 0xac, 0x14, 0xc4, 0x9e, 0xef, 0x4b, 0x73, 0x1b, 0x56, 0x70, 0x6e, 0xe1, 0x5e, 0x91, 0xa4, - 0x56, 0x4a, 0x75, 0xff, 0xe0, 0xb5, 0x6d, 0xb7, 0x7c, 0x3d, 0x6b, 0x19, 0x2f, 0xb3, 0x96, 0x61, - 0x5b, 0xb0, 0xb9, 0x29, 0xd8, 0x90, 0xa8, 0x50, 0x70, 0x45, 0x3a, 0xf7, 0x00, 0x96, 0x06, 0x8a, - 0x9a, 0xb7, 0x00, 0xfe, 0xff, 0x1c, 0xff, 0xc8, 0xf9, 0xc2, 0x3f, 0x3b, 0x9b, 0x0c, 0x1a, 0xbd, - 0x6f, 0x3f, 0x2d, 0xb2, 0xf5, 0xcf, 0x1f, 0x16, 0x16, 0x98, 0x2f, 0x2c, 0xf0, 0xbc, 0xb0, 0xc0, - 0xcd, 0xd2, 0x32, 0xe6, 0x4b, 0xcb, 0x78, 0x5c, 0x5a, 0xc6, 0xc5, 0x31, 0x65, 0x7a, 0x1c, 0x8d, - 0x1c, 0x2c, 0x82, 0x7c, 0x47, 0x68, 0xed, 0xb6, 0xff, 0x7e, 0x3e, 0x71, 0x07, 0x4d, 0x3f, 0xde, - 0x90, 0x4e, 0x42, 0xa2, 0x46, 0x3f, 0xd3, 0xad, 0x1d, 0xbc, 0x05, 0x00, 0x00, 0xff, 0xff, 0x9a, - 0x29, 0xaf, 0xde, 0x74, 0x02, 0x00, 0x00, + // 623 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0xcf, 0x4f, 0xd4, 0x40, + 0x14, 0xde, 0x42, 0xa2, 0x30, 0xa0, 0x89, 0x0d, 0x04, 0xd8, 0x60, 0x57, 0xd7, 0x28, 0x1e, 0x70, + 0x26, 0xac, 0x07, 0x23, 0x89, 0x07, 0x56, 0x4c, 0xfc, 0x91, 0x4d, 0xcc, 0x9a, 0x60, 0xe2, 0xc1, + 0xa6, 0x9d, 0x3e, 0xba, 0x13, 0xda, 0x99, 0xcd, 0xcc, 0xb4, 0x61, 0xff, 0x03, 0x8e, 0x7a, 0x32, + 0xde, 0xf8, 0x03, 0xfc, 0x43, 0x3c, 0x72, 0xf4, 0x64, 0x0c, 0x5c, 0x3c, 0x7b, 0xf1, 0x6a, 0x3a, + 0x6d, 0xd9, 0x12, 0x2b, 0x90, 0xf5, 0xd6, 0x79, 0xef, 0x9b, 0xef, 0x7d, 0xdf, 0x9b, 0xd7, 0x87, + 0xd6, 0x19, 0xd7, 0x20, 0xe9, 0xc0, 0x63, 0xdc, 0x55, 0x40, 0x13, 0xc9, 0xf4, 0x88, 0x50, 0x9a, + 0x92, 0xa1, 0x14, 0x29, 0x0b, 0x40, 0x92, 0x74, 0x83, 0xe8, 0x7d, 0x3c, 0x94, 0x42, 0x0b, 0xfb, + 0x4e, 0x0d, 0x1a, 0x53, 0x9a, 0xe2, 0x12, 0x8d, 0xd3, 0x8d, 0xe6, 0x6a, 0x28, 0x44, 0x18, 0x01, + 0xf1, 0x86, 0x8c, 0x78, 0x9c, 0x0b, 0xed, 0x69, 0x26, 0xb8, 0xca, 0x29, 0x9a, 0x0b, 0xa1, 0x08, + 0x85, 0xf9, 0x24, 0xd9, 0x57, 0x11, 0x5d, 0xa1, 0x42, 0xc5, 0x42, 0xb9, 0x79, 0x22, 0x3f, 0x94, + 0xa9, 0x82, 0xce, 0x9c, 0xfc, 0x64, 0x97, 0x78, 0x7c, 0x54, 0xa4, 0x08, 0xf3, 0x29, 0x89, 0x58, + 0x38, 0xd0, 0x34, 0x62, 0xc0, 0xb5, 0x22, 0x1a, 0x78, 0x00, 0x32, 0x66, 0x5c, 0x1b, 0xdd, 0xa7, + 0xa7, 0xe2, 0x42, 0xab, 0x92, 0xd7, 0xa3, 0x21, 0x28, 0x02, 0x99, 0x6c, 0x4e, 0x21, 0x07, 0xb4, + 0x3f, 0x59, 0x68, 0xa1, 0xa7, 0xc2, 0x2d, 0xa5, 0x58, 0xc8, 0x9f, 0x0a, 0xae, 0x92, 0x18, 0xe4, + 0x2b, 0x18, 0xd9, 0x2b, 0x68, 0x26, 0xb7, 0xcd, 0x82, 0x65, 0xeb, 0x96, 0x75, 0x7f, 0xb6, 0x7f, + 0xd5, 0x9c, 0x5f, 0x04, 0xf6, 0x23, 0x74, 0xad, 0xb4, 0xef, 0x7a, 0x41, 0x20, 0x97, 0xa7, 0xb2, + 0x7c, 0xd7, 0xfe, 0xf5, 0xbd, 0x75, 0x7d, 0xe4, 0xc5, 0xd1, 0x66, 0x3b, 0x8b, 0x82, 0x52, 0xed, + 0xfe, 0x7c, 0x09, 0xdc, 0x0a, 0x02, 0x69, 0xdf, 0x46, 0xf3, 0xb4, 0x28, 0xe1, 0xee, 0xc1, 0x68, + 0x79, 0xda, 0xf0, 0xce, 0xd1, 0x71, 0xd9, 0xcd, 0x99, 0x83, 0xc3, 0x56, 0xe3, 0xe7, 0x61, 0xab, + 0xd1, 0x76, 0xd0, 0x6a, 0x9d, 0xb0, 0x3e, 0xa8, 0xa1, 0xe0, 0x0a, 0xda, 0x9f, 0x2d, 0x74, 0xb3, + 0xa7, 0xc2, 0x37, 0x89, 0x1f, 0x33, 0x5d, 0x02, 0x7a, 0x4c, 0xf9, 0x30, 0xf0, 0x52, 0x26, 0x12, + 0x69, 0xaf, 0xa2, 0x59, 0x65, 0xb2, 0x1a, 0x64, 0xe1, 0x61, 0x1c, 0xb0, 0x5f, 0xa3, 0xf9, 0xb8, + 0x82, 0x36, 0x26, 0xe6, 0x3a, 0xeb, 0x98, 0xf9, 0x14, 0x57, 0x5b, 0x8c, 0x2b, 0x4d, 0x4d, 0x37, + 0x70, 0xb5, 0x42, 0xff, 0x0c, 0x43, 0x45, 0xfb, 0x1a, 0xba, 0x7b, 0xae, 0xb4, 0x53, 0x13, 0x07, + 0x53, 0x35, 0x26, 0xb6, 0x45, 0xe2, 0x47, 0xb0, 0x23, 0x34, 0xe3, 0xe1, 0x05, 0x26, 0x5c, 0xb4, + 0x14, 0x24, 0xc3, 0x88, 0x51, 0x4f, 0x83, 0x9b, 0x0a, 0x0d, 0x6e, 0xf9, 0xbe, 0x85, 0x9f, 0xb5, + 0xaa, 0x7c, 0x33, 0x01, 0x78, 0xbb, 0xbc, 0xb0, 0x23, 0x34, 0x3c, 0x2b, 0xe0, 0xfd, 0xc5, 0xa0, + 0x2e, 0x6c, 0xbf, 0x47, 0x4b, 0x8c, 0xef, 0x4a, 0x8f, 0x66, 0x23, 0xed, 0xfa, 0x91, 0xa0, 0x7b, + 0xee, 0x00, 0xbc, 0x00, 0xa4, 0x79, 0xbd, 0xb9, 0xce, 0xbd, 0x8b, 0x1a, 0xf6, 0xdc, 0xa0, 0xfb, + 0x8b, 0x63, 0x9a, 0x6e, 0xc6, 0x92, 0x87, 0x2f, 0xe8, 0x59, 0xb5, 0x13, 0x65, 0xcf, 0x3a, 0xbf, + 0xa7, 0xd1, 0x74, 0x4f, 0x85, 0xf6, 0x47, 0x0b, 0xdd, 0xf8, 0x7b, 0x6e, 0x1f, 0xe3, 0x4b, 0xfc, + 0xb2, 0xb8, 0x6e, 0xb2, 0x9a, 0x5b, 0x13, 0x5f, 0x2d, 0xb5, 0xd9, 0x5f, 0x2c, 0xd4, 0x3c, 0x67, + 0x22, 0xbb, 0x97, 0xad, 0xf0, 0x6f, 0x8e, 0xe6, 0xcb, 0xff, 0xe7, 0x38, 0x47, 0xee, 0x99, 0xd9, + 0x9b, 0x50, 0x6e, 0x95, 0x63, 0x52, 0xb9, 0x75, 0x2f, 0xdf, 0x7d, 0xfb, 0xf5, 0xd8, 0xb1, 0x8e, + 0x8e, 0x1d, 0xeb, 0xc7, 0xb1, 0x63, 0x7d, 0x38, 0x71, 0x1a, 0x47, 0x27, 0x4e, 0xe3, 0xdb, 0x89, + 0xd3, 0x78, 0xf7, 0x24, 0x64, 0x7a, 0x90, 0xf8, 0x98, 0x8a, 0xb8, 0x58, 0xa6, 0x64, 0x5c, 0xf6, + 0xc1, 0xe9, 0x9e, 0x4f, 0x3b, 0x64, 0xff, 0xec, 0xb2, 0x37, 0xbf, 0x84, 0x7f, 0xc5, 0x2c, 0xc3, + 0x87, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0xf6, 0x8f, 0xf4, 0xef, 0x1d, 0x06, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -161,6 +341,8 @@ const _ = grpc.SupportPackageIsVersion4 // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type MsgClient interface { AssignConsumerKey(ctx context.Context, in *MsgAssignConsumerKey, opts ...grpc.CallOption) (*MsgAssignConsumerKeyResponse, 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 { @@ -180,9 +362,29 @@ func (c *msgClient) AssignConsumerKey(ctx context.Context, in *MsgAssignConsumer 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 +} + +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) + SubmitConsumerMisbehaviour(context.Context, *MsgSubmitConsumerMisbehaviour) (*MsgSubmitConsumerMisbehaviourResponse, error) + SubmitConsumerDoubleVoting(context.Context, *MsgSubmitConsumerDoubleVoting) (*MsgSubmitConsumerDoubleVotingResponse, error) } // UnimplementedMsgServer can be embedded to have forward compatible implementations. @@ -192,6 +394,12 @@ type UnimplementedMsgServer struct { func (*UnimplementedMsgServer) AssignConsumerKey(ctx context.Context, req *MsgAssignConsumerKey) (*MsgAssignConsumerKeyResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AssignConsumerKey not implemented") } +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) @@ -215,6 +423,42 @@ func _Msg_AssignConsumerKey_Handler(srv interface{}, ctx context.Context, dec fu 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) +} + +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), @@ -223,6 +467,14 @@ var _Msg_serviceDesc = grpc.ServiceDesc{ MethodName: "AssignConsumerKey", Handler: _Msg_AssignConsumerKey_Handler, }, + { + MethodName: "SubmitConsumerMisbehaviour", + Handler: _Msg_SubmitConsumerMisbehaviour_Handler, + }, + { + MethodName: "SubmitConsumerDoubleVoting", + Handler: _Msg_SubmitConsumerDoubleVoting_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "interchain_security/ccv/provider/v1/tx.proto", @@ -295,6 +547,148 @@ func (m *MsgAssignConsumerKeyResponse) MarshalToSizedBuffer(dAtA []byte) (int, e 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 (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 @@ -336,16 +730,72 @@ func (m *MsgAssignConsumerKeyResponse) Size() (n int) { return n } -func sovTx(x uint64) (n int) { - return (math_bits.Len64(x|1) + 6) / 7 -} -func sozTx(x uint64) (n int) { - return sovTx(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +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 *MsgAssignConsumerKey) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { + +func (m *MsgSubmitConsumerMisbehaviourResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + 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 +} +func sozTx(x uint64) (n int) { + return sovTx(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *MsgAssignConsumerKey) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { @@ -538,6 +988,378 @@ func (m *MsgAssignConsumerKeyResponse) 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 (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..4fbb65398a 100644 --- a/x/ccv/types/errors.go +++ b/x/ccv/types/errors.go @@ -6,23 +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") + 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 23962fbbaf..07c9f9ba59 100644 --- a/x/ccv/types/events.go +++ b/x/ccv/types/events.go @@ -2,19 +2,20 @@ package types // CCV events const ( - EventTypeTimeout = "timeout" - EventTypePacket = "ccv_packet" - EventTypeChannelEstablished = "channel_established" - EventTypeFeeTransferChannelOpened = "fee_transfer_channel_opened" - EventTypeConsumerClientCreated = "consumer_client_created" - EventTypeAssignConsumerKey = "assign_consumer_key" - EventTypeAddConsumerRewardDenom = "add_consumer_reward_denom" - EventTypeRemoveConsumerRewardDenom = "remove_consumer_reward_denom" - - EventTypeExecuteConsumerChainSlash = "execute_consumer_chain_slash" - EventTypeFeeDistribution = "fee_distribution" - EventTypeConsumerSlashRequest = "consumer_slash_request" - EventTypeVSCMatured = "vsc_matured" + EventTypeTimeout = "timeout" + EventTypePacket = "ccv_packet" + EventTypeChannelEstablished = "channel_established" + EventTypeFeeTransferChannelOpened = "fee_transfer_channel_opened" + EventTypeConsumerClientCreated = "consumer_client_created" + EventTypeAssignConsumerKey = "assign_consumer_key" + EventTypeSubmitConsumerMisbehaviour = "submit_consumer_misbehaviour" + EventTypeSubmitConsumerDoubleVoting = "submit_consumer_double_voting" + EventTypeExecuteConsumerChainSlash = "execute_consumer_chain_slash" + EventTypeFeeDistribution = "fee_distribution" + EventTypeConsumerSlashRequest = "consumer_slash_request" + EventTypeVSCMatured = "vsc_matured" + EventTypeAddConsumerRewardDenom = "add_consumer_reward_denom" + EventTypeRemoveConsumerRewardDenom = "remove_consumer_reward_denom" AttributeKeyAckSuccess = "success" AttributeKeyAck = "acknowledgement" @@ -34,6 +35,12 @@ 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" + AttributeConsumerDoubleVoting = "consumer_double_voting" 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 dd81c5e801..4169d510e6 100644 --- a/x/ccv/types/expected_keepers.go +++ b/x/ccv/types/expected_keepers.go @@ -87,6 +87,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 d92a4c8e184169bd7a8540d0d2309bb6d922dc7a Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Thu, 14 Sep 2023 16:16:48 +0200 Subject: [PATCH 12/29] undelegations are getting slashed integraiton test --- tests/integration/double_vote.go | 110 ++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index cc576055bd..6aab07749e 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -10,7 +10,7 @@ import ( ) // TestHandleConsumerDoubleVoting verifies that handling a double voting evidence -// of a consumer chain results in the expected tombstoning, jailing, and slashing of the malicious validator +// of a consumer chain results in the expected tombstoning and jailing the misbehaved validator func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { s.SetupCCVChannel(s.path) // required to have the consumer client revision height greater than 0 @@ -24,7 +24,6 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { 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()] provValSet, err := tmtypes.ValidatorSetFromProto(s.providerChain.LastHeader.ValidatorSet) @@ -207,3 +206,110 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { }) } } + +// TestHandleConsumerDoubleVotingSlashesUndelegations verifies that handling a successful double voting +// evidence of a consumer chain results in the expected slashing of the misbehave validator undelegations +func (s *CCVTestSuite) TestHandleConsumerDoubleVotingSlashesUndelegations() { + 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) + } + + consuValSet, err := tmtypes.ValidatorSetFromProto(s.consumerChain.LastHeader.ValidatorSet) + s.Require().NoError(err) + consuVal := consuValSet.Validators[0] + consuSigner := s.consumerChain.Signers[consuVal.Address.String()] + + blockID1 := testutil.MakeBlockID([]byte("blockhash"), 1000, []byte("partshash")) + blockID2 := testutil.MakeBlockID([]byte("blockhash2"), 1000, []byte("partshash")) + + // 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, + ) + + // 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 + evidence := &tmtypes.DuplicateVoteEvidence{ + VoteA: consuVote, + VoteB: consuBadVote, + ValidatorPower: consuVal.VotingPower, + TotalVotingPower: consuVal.VotingPower, + Timestamp: s.consumerCtx().BlockTime(), + } + + chainID := s.consumerChain.ChainID + pubKey := consuVal.PubKey + + consuAddr := types.NewConsumerConsAddress(sdk.ConsAddress(consuVal.Address.Bytes())) + provAddr := s.providerApp.GetProviderKeeper().GetProviderAddrFromConsumerAddr(s.providerCtx(), s.consumerChain.ChainID, consuAddr) + + validator, found := s.providerApp.GetTestStakingKeeper().GetValidator(s.providerCtx(), provAddr.ToSdkConsAddr().Bytes()) + s.Require().True(found) + + s.Run("slash undelegations when getting double voting evidence", func() { + // convert validator public key + pk, err := cryptocodec.FromTmPubKeyInterface(pubKey) + s.Require().NoError(err) + + // perform a delegation and an undelegation of the whole amount + bondAmt := sdk.NewInt(10000000) + delAddr := s.providerChain.SenderAccount.GetAddress() + + // in order to perform a delegation we need to know the validator's `idx` (that might not be 0) + // loop through all validators to find the right `idx` + idx := 0 + for i := 0; i <= len(s.providerChain.Vals.Validators); i = i + 1 { + _, valAddr := s.getValByIdx(i) + if validator.OperatorAddress == valAddr.String() { + idx = i + break + } + } + + _, shares, valAddr := delegateByIdx(s, delAddr, bondAmt, idx) + _ = undelegate(s, delAddr, valAddr, shares) + + _, shares, _ = delegateByIdx(s, delAddr, sdk.NewInt(50000000), idx) + _ = undelegate(s, delAddr, valAddr, shares) + + err = s.providerApp.GetProviderKeeper().HandleConsumerDoubleVoting( + s.providerCtx(), + evidence, + chainID, + pk, + ) + s.Require().NoError(err) + + slashFraction := s.providerApp.GetTestSlashingKeeper().SlashFractionDoubleSign(s.providerCtx()) + + // check undelegations are slashed + ubds, _ := s.providerApp.GetTestStakingKeeper().GetUnbondingDelegation(s.providerCtx(), delAddr, validator.GetOperator()) + s.Require().True(len(ubds.Entries) > 0) + for _, unb := range ubds.Entries { + initialBalance := unb.InitialBalance.ToDec() + currentBalance := unb.Balance.ToDec() + s.Require().True(initialBalance.Sub(initialBalance.Mul(slashFraction)).Equal(currentBalance)) + } + }) +} From b46ace4b79d10985997577f54e67ca36ca84b4ba Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Thu, 14 Sep 2023 17:05:11 +0200 Subject: [PATCH 13/29] fix merge issues --- tests/e2e/actions.go | 2 +- x/ccv/provider/client/cli/tx.go | 91 -- x/ccv/provider/handler.go | 6 - x/ccv/provider/keeper/double_vote.go | 1 - x/ccv/provider/keeper/msg_server.go | 69 -- x/ccv/provider/keeper/punish_validator.go | 2 +- .../provider/keeper/punish_validator_test.go | 4 +- x/ccv/provider/types/tx.pb.go | 823 +----------------- 8 files changed, 17 insertions(+), 981 deletions(-) diff --git a/tests/e2e/actions.go b/tests/e2e/actions.go index 7a9aa9bc9e..4419d1dcff 100644 --- a/tests/e2e/actions.go +++ b/tests/e2e/actions.go @@ -525,7 +525,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 { diff --git a/x/ccv/provider/client/cli/tx.go b/x/ccv/provider/client/cli/tx.go index b1658c5be4..f32bc304df 100644 --- a/x/ccv/provider/client/cli/tx.go +++ b/x/ccv/provider/client/cli/tx.go @@ -169,94 +169,3 @@ Examples: 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 -} - -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, &header) - 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 66ff6ed593..b67f3fbb31 100644 --- a/x/ccv/provider/handler.go +++ b/x/ccv/provider/handler.go @@ -24,12 +24,6 @@ func NewHandler(k *keeper.Keeper) sdk.Handler { case *types.MsgSubmitConsumerDoubleVoting: res, err := msgServer.SubmitConsumerDoubleVoting(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) - 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 index bfb0cadac0..bb48767bd6 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -3,7 +3,6 @@ package keeper import ( "bytes" "fmt" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/ccv/provider/keeper/msg_server.go b/x/ccv/provider/keeper/msg_server.go index b11c08248f..a24d6ef919 100644 --- a/x/ccv/provider/keeper/msg_server.go +++ b/x/ccv/provider/keeper/msg_server.go @@ -176,72 +176,3 @@ func (k msgServer) SubmitConsumerDoubleVoting(goCtx context.Context, msg *types. return &types.MsgSubmitConsumerDoubleVotingResponse{}, 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 -} - -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 - } - - // 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{ - 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 index d1dc8b8ead..802409b667 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -4,7 +4,7 @@ import ( 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" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" "time" ) diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index 47f4323e0f..0a3005f58b 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -4,7 +4,7 @@ import ( "fmt" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" - sdk "github.com/cosmos/cosmos-sdk/types" + 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" @@ -12,7 +12,7 @@ import ( "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/require" tmtypes "github.com/tendermint/tendermint/types" "testing" "time" diff --git a/x/ccv/provider/types/tx.pb.go b/x/ccv/provider/types/tx.pb.go index ad7752d429..b4e78f90d4 100644 --- a/x/ccv/provider/types/tx.pb.go +++ b/x/ccv/provider/types/tx.pb.go @@ -272,165 +272,6 @@ func (m *MsgSubmitConsumerDoubleVotingResponse) XXX_DiscardUnknown() { var xxx_messageInfo_MsgSubmitConsumerDoubleVotingResponse 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 - -// 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") @@ -539,24 +380,6 @@ func (c *msgClient) SubmitConsumerDoubleVoting(ctx context.Context, in *MsgSubmi 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 -} - -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) @@ -577,12 +400,6 @@ func (*UnimplementedMsgServer) SubmitConsumerMisbehaviour(ctx context.Context, r func (*UnimplementedMsgServer) SubmitConsumerDoubleVoting(ctx context.Context, req *MsgSubmitConsumerDoubleVoting) (*MsgSubmitConsumerDoubleVotingResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SubmitConsumerDoubleVoting not implemented") } -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) @@ -642,42 +459,6 @@ func _Msg_SubmitConsumerDoubleVoting_Handler(srv interface{}, ctx context.Contex 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) -} - -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), @@ -694,14 +475,6 @@ var _Msg_serviceDesc = grpc.ServiceDesc{ MethodName: "SubmitConsumerDoubleVoting", Handler: _Msg_SubmitConsumerDoubleVoting_Handler, }, - { - MethodName: "SubmitConsumerMisbehaviour", - Handler: _Msg_SubmitConsumerMisbehaviour_Handler, - }, - { - MethodName: "SubmitConsumerDoubleVoting", - Handler: _Msg_SubmitConsumerDoubleVoting_Handler, - }, }, Streams: []grpc.StreamDesc{}, Metadata: "interchain_security/ccv/provider/v1/tx.proto", @@ -916,163 +689,21 @@ func (m *MsgSubmitConsumerDoubleVotingResponse) MarshalToSizedBuffer(dAtA []byte 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 +func encodeVarintTx(dAtA []byte, offset int, v uint64) int { + offset -= sovTx(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ } - return dAtA[:n], nil -} - -func (m *MsgSubmitConsumerMisbehaviour) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) + dAtA[offset] = uint8(v) + return base } - -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 (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 - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *MsgAssignConsumerKey) Size() (n int) { - if m == nil { - return 0 - } +func (m *MsgAssignConsumerKey) Size() (n int) { + if m == nil { + return 0 + } var l int _ = l l = len(m.ChainId) @@ -1155,62 +786,6 @@ func (m *MsgSubmitConsumerDoubleVotingResponse) 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 (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 } @@ -1785,378 +1360,6 @@ func (m *MsgSubmitConsumerDoubleVotingResponse) 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 (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 From 59d8192f8c163e21031a7b00487ef28601780650 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Thu, 14 Sep 2023 17:14:36 +0200 Subject: [PATCH 14/29] fix mocks --- testutil/keeper/mocks.go | 48 ++++++++++------------------------------ 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/testutil/keeper/mocks.go b/testutil/keeper/mocks.go index ef310adddc..3df4110286 100644 --- a/testutil/keeper/mocks.go +++ b/testutil/keeper/mocks.go @@ -12,14 +12,13 @@ import ( types "github.com/cosmos/cosmos-sdk/types" types0 "github.com/cosmos/cosmos-sdk/x/auth/types" types1 "github.com/cosmos/cosmos-sdk/x/capability/types" - exported "github.com/cosmos/cosmos-sdk/x/evidence/exported" types2 "github.com/cosmos/cosmos-sdk/x/evidence/types" types3 "github.com/cosmos/cosmos-sdk/x/slashing/types" types4 "github.com/cosmos/cosmos-sdk/x/staking/types" types5 "github.com/cosmos/ibc-go/v4/modules/core/02-client/types" types6 "github.com/cosmos/ibc-go/v4/modules/core/03-connection/types" types7 "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" - exported0 "github.com/cosmos/ibc-go/v4/modules/core/exported" + exported "github.com/cosmos/ibc-go/v4/modules/core/exported" gomock "github.com/golang/mock/gomock" types8 "github.com/tendermint/tendermint/abci/types" ) @@ -143,6 +142,7 @@ func (m *MockStakingKeeper) GetUnbondingDelegationsFromValidator(ctx types.Conte func (mr *MockStakingKeeperMockRecorder) GetUnbondingDelegationsFromValidator(ctx, valAddr interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnbondingDelegationsFromValidator", reflect.TypeOf((*MockStakingKeeper)(nil).GetUnbondingDelegationsFromValidator), ctx, valAddr) +} // GetUnbondingType mocks base method. func (m *MockStakingKeeper) GetUnbondingType(ctx types.Context, id uint64) (types4.UnbondingType, bool) { @@ -410,18 +410,6 @@ func (mr *MockEvidenceKeeperMockRecorder) HandleEquivocationEvidence(ctx, eviden return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleEquivocationEvidence", reflect.TypeOf((*MockEvidenceKeeper)(nil).HandleEquivocationEvidence), ctx, evidence) } -// SetEvidence mocks base method. -func (m *MockEvidenceKeeper) SetEvidence(ctx types.Context, evidence exported.Evidence) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetEvidence", ctx, evidence) -} - -// SetEvidence indicates an expected call of SetEvidence. -func (mr *MockEvidenceKeeperMockRecorder) SetEvidence(ctx, evidence interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEvidence", reflect.TypeOf((*MockEvidenceKeeper)(nil).SetEvidence), ctx, evidence) -} - // MockSlashingKeeper is a mock of SlashingKeeper interface. type MockSlashingKeeper struct { ctrl *gomock.Controller @@ -608,7 +596,7 @@ func (mr *MockChannelKeeperMockRecorder) GetNextSequenceSend(ctx, portID, channe } // SendPacket mocks base method. -func (m *MockChannelKeeper) SendPacket(ctx types.Context, channelCap *types1.Capability, packet exported0.PacketI) error { +func (m *MockChannelKeeper) SendPacket(ctx types.Context, channelCap *types1.Capability, packet exported.PacketI) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SendPacket", ctx, channelCap, packet) ret0, _ := ret[0].(error) @@ -622,7 +610,7 @@ func (mr *MockChannelKeeperMockRecorder) SendPacket(ctx, channelCap, packet inte } // WriteAcknowledgement mocks base method. -func (m *MockChannelKeeper) WriteAcknowledgement(ctx types.Context, chanCap *types1.Capability, packet exported0.PacketI, acknowledgement exported0.Acknowledgement) error { +func (m *MockChannelKeeper) WriteAcknowledgement(ctx types.Context, chanCap *types1.Capability, packet exported.PacketI, acknowledgement exported.Acknowledgement) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "WriteAcknowledgement", ctx, chanCap, packet, acknowledgement) ret0, _ := ret[0].(error) @@ -762,7 +750,7 @@ func (mr *MockClientKeeperMockRecorder) ClientStore(ctx, clientID interface{}) * } // CreateClient mocks base method. -func (m *MockClientKeeper) CreateClient(ctx types.Context, clientState exported0.ClientState, consensusState exported0.ConsensusState) (string, error) { +func (m *MockClientKeeper) CreateClient(ctx types.Context, clientState exported.ClientState, consensusState exported.ConsensusState) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateClient", ctx, clientState, consensusState) ret0, _ := ret[0].(string) @@ -792,10 +780,10 @@ func (mr *MockClientKeeperMockRecorder) GetClientConsensusState(ctx, clientID, h } // GetClientState mocks base method. -func (m *MockClientKeeper) GetClientState(ctx types.Context, clientID string) (exported0.ClientState, bool) { +func (m *MockClientKeeper) GetClientState(ctx types.Context, clientID string) (exported.ClientState, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetClientState", ctx, clientID) - ret0, _ := ret[0].(exported0.ClientState) + ret0, _ := ret[0].(exported.ClientState) ret1, _ := ret[1].(bool) return ret0, ret1 } @@ -807,10 +795,10 @@ func (mr *MockClientKeeperMockRecorder) GetClientState(ctx, clientID interface{} } // GetLatestClientConsensusState mocks base method. -func (m *MockClientKeeper) GetLatestClientConsensusState(ctx types.Context, clientID string) (exported0.ConsensusState, bool) { +func (m *MockClientKeeper) GetLatestClientConsensusState(ctx types.Context, clientID string) (exported.ConsensusState, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetLatestClientConsensusState", ctx, clientID) - ret0, _ := ret[0].(exported0.ConsensusState) + ret0, _ := ret[0].(exported.ConsensusState) ret1, _ := ret[1].(bool) return ret0, ret1 } @@ -822,10 +810,10 @@ func (mr *MockClientKeeperMockRecorder) GetLatestClientConsensusState(ctx, clien } // GetSelfConsensusState mocks base method. -func (m *MockClientKeeper) GetSelfConsensusState(ctx types.Context, height exported0.Height) (exported0.ConsensusState, error) { +func (m *MockClientKeeper) GetSelfConsensusState(ctx types.Context, height exported.Height) (exported.ConsensusState, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSelfConsensusState", ctx, height) - ret0, _ := ret[0].(exported0.ConsensusState) + ret0, _ := ret[0].(exported.ConsensusState) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -837,7 +825,7 @@ func (mr *MockClientKeeperMockRecorder) GetSelfConsensusState(ctx, height interf } // SetClientState mocks base method. -func (m *MockClientKeeper) SetClientState(ctx types.Context, clientID string, clientState exported0.ClientState) { +func (m *MockClientKeeper) SetClientState(ctx types.Context, clientID string, clientState exported.ClientState) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetClientState", ctx, clientID, clientState) } @@ -885,18 +873,6 @@ 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 From 31c090d933288d818b9c4580f9e1c5efef8400bc Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Thu, 14 Sep 2023 17:26:15 +0200 Subject: [PATCH 15/29] fix lint issue --- tests/integration/double_vote.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index 8f4c35fa29..778d98080a 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -278,7 +278,7 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVotingSlashesUndelegations() { // in order to perform a delegation we need to know the validator's `idx` (that might not be 0) // loop through all validators to find the right `idx` idx := 0 - for i := 0; i <= len(s.providerChain.Vals.Validators); i = i + 1 { + for i := 0; i <= len(s.providerChain.Vals.Validators); i++ { _, valAddr := s.getValByIdx(i) if validator.OperatorAddress == valAddr.String() { idx = i @@ -311,4 +311,4 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVotingSlashesUndelegations() { s.Require().True(initialBalance.Sub(initialBalance.Mul(slashFraction)).Equal(currentBalance)) } }) -} \ No newline at end of file +} From ed3563835f25cc632c7d75e8aaea8dedd5f65299 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Fri, 22 Sep 2023 11:15:55 +0200 Subject: [PATCH 16/29] took into account Simon's comments --- go.sum | 2 + x/ccv/provider/keeper/misbehaviour.go | 4 +- x/ccv/provider/keeper/punish_validator.go | 13 +++-- .../provider/keeper/punish_validator_test.go | 47 ++++++++++++++++++- 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/go.sum b/go.sum index 3d7480161f..0378b823b2 100644 --- a/go.sum +++ b/go.sum @@ -1128,6 +1128,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1380,6 +1381,7 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 8057320875..980562be44 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -34,14 +34,14 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty provAddrs := make([]types.ProviderConsAddress, len(byzantineValidators)) - // jail the Byzantine validators + // slash, jail, and tombstone the Byzantine validators for _, v := range byzantineValidators { providerAddr := k.GetProviderAddrFromConsumerAddr( ctx, misbehaviour.Header1.Header.ChainID, types.NewConsumerConsAddress(sdk.ConsAddress(v.Address.Bytes())), ) - k.SlashValidator(ctx, providerAddr) + k.SlashValidator(ctx, providerAddr) k.JailAndTombstoneValidator(ctx, providerAddr) provAddrs = append(provAddrs, providerAddr) } diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index 802409b667..84e3511c56 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -84,20 +84,25 @@ func (k Keeper) ComputePowerToSlash(now time.Time, undelegations []stakingtypes. func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) { logger := k.Logger(ctx) - val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) + validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) if !found { logger.Error("validator not found", "provider consensus address", providerAddr.String()) return } + if validator.IsUnbonded() { + logger.Info("validator is unbonded", "provider consensus address", providerAddr.String()) + return + } + if k.slashingKeeper.IsTombstoned(ctx, providerAddr.ToSdkConsAddr()) { logger.Info("validator is already tombstoned", "provider consensus address", providerAddr.String()) return } - undelegations := k.stakingKeeper.GetUnbondingDelegationsFromValidator(ctx, val.GetOperator()) - redelegations := k.stakingKeeper.GetRedelegationsFromSrcValidator(ctx, val.GetOperator()) - lastPower := k.stakingKeeper.GetLastValidatorPower(ctx, val.GetOperator()) + undelegations := k.stakingKeeper.GetUnbondingDelegationsFromValidator(ctx, validator.GetOperator()) + redelegations := k.stakingKeeper.GetRedelegationsFromSrcValidator(ctx, validator.GetOperator()) + lastPower := k.stakingKeeper.GetLastValidatorPower(ctx, validator.GetOperator()) powerReduction := k.stakingKeeper.PowerReduction(ctx) totalPower := k.ComputePowerToSlash(ctx.BlockHeader().Time, undelegations, redelegations, lastPower, powerReduction) slashFraction := k.slashingKeeper.SlashFractionDoubleSign(ctx) diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index 0a3005f58b..c31f0f03a7 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "fmt" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -317,8 +318,25 @@ func TestSlashValidator(t *testing.T) { testkeeper.NewInMemProviderKeeper(keeperParams, mocks) pubKey, _ := cryptocodec.FromTmPubKeyInterface(tmtypes.NewMockPV().PrivKey.PubKey()) + pkAny, _ := codectypes.NewAnyWithValue(pubKey) + + // manually build a validator instead of using `stakingtypes.NewValidator` to guarantee that the validator is bonded + validator := stakingtypes.Validator{ + OperatorAddress: sdk.ValAddress(pubKey.Address().Bytes()).String(), + ConsensusPubkey: pkAny, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: sdk.ZeroInt(), + DelegatorShares: sdk.ZeroDec(), + Description: stakingtypes.Description{}, + UnbondingHeight: int64(0), + UnbondingTime: time.Unix(0, 0).UTC(), + Commission: stakingtypes.NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + UnbondingOnHoldRefCount: 0, + ValidatorBondShares: sdk.ZeroDec(), + LiquidShares: sdk.ZeroDec(), + } - validator, _ := stakingtypes.NewValidator(pubKey.Address().Bytes(), pubKey, stakingtypes.Description{}) consAddr, _ := validator.GetConsAddr() providerAddr := types.NewProviderConsAddress(consAddr) @@ -372,3 +390,30 @@ func TestSlashValidator(t *testing.T) { gomock.InOrder(expectedCalls...) keeper.SlashValidator(ctx, providerAddr) } + +// TestSlashValidatorDoesNotSlashIfValidatorIsUnbonded asserts that `SlashValidator` does not call +// the staking module's `Slash` method if the validator to be slashed is unbonded +func TestSlashValidatorDoesNotSlashIfValidatorIsUnbonded(t *testing.T) { + keeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + keeperParams := testkeeper.NewInMemKeeperParams(t) + testkeeper.NewInMemProviderKeeper(keeperParams, mocks) + + pubKey, _ := cryptocodec.FromTmPubKeyInterface(tmtypes.NewMockPV().PrivKey.PubKey()) + + // validator is initially unbonded + validator, _ := stakingtypes.NewValidator(pubKey.Address().Bytes(), pubKey, stakingtypes.Description{}) + + consAddr, _ := validator.GetConsAddr() + providerAddr := types.NewProviderConsAddress(consAddr) + + expectedCalls := []*gomock.Call{ + mocks.MockStakingKeeper.EXPECT(). + GetValidatorByConsAddr(ctx, gomock.Any()). + Return(validator, true), + } + + gomock.InOrder(expectedCalls...) + keeper.SlashValidator(ctx, providerAddr) +} From 62212c715f362b25571bb0946bc56363d3f9f228 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Fri, 22 Sep 2023 13:11:55 +0200 Subject: [PATCH 17/29] go.sum changes --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 0378b823b2..3d7480161f 100644 --- a/go.sum +++ b/go.sum @@ -1128,7 +1128,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1381,7 +1380,6 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From e5b46dd2720b7e33f3cc8cc18b8fa640fc28e38c Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Fri, 22 Sep 2023 13:15:46 +0200 Subject: [PATCH 18/29] gosec fix --- x/ccv/consumer/types/keys.go | 1 + x/ccv/types/events.go | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/x/ccv/consumer/types/keys.go b/x/ccv/consumer/types/keys.go index 093f78b450..fb35626698 100644 --- a/x/ccv/consumer/types/keys.go +++ b/x/ccv/consumer/types/keys.go @@ -25,6 +25,7 @@ const ( ConsumerRedistributeName = "cons_redistribute" // ConsumerToSendToProviderName is a "buffer" address for outgoing fees to be transferred to the provider chain + //#nosec G101 -- (false positive) this is not a hardcoded credential ConsumerToSendToProviderName = "cons_to_send_to_provider" ) diff --git a/x/ccv/types/events.go b/x/ccv/types/events.go index 07c9f9ba59..1ee1817554 100644 --- a/x/ccv/types/events.go +++ b/x/ccv/types/events.go @@ -43,10 +43,11 @@ const ( AttributeConsumerDoubleVoting = "consumer_double_voting" AttributeDistributionCurrentHeight = "current_distribution_height" - AttributeDistributionNextHeight = "next_distribution_height" - AttributeDistributionFraction = "distribution_fraction" - AttributeDistributionTotal = "total" - AttributeDistributionToProvider = "provider_amount" + //#nosec G101 -- (false positive) this is not a hardcoded credential + AttributeDistributionNextHeight = "next_distribution_height" + AttributeDistributionFraction = "distribution_fraction" + AttributeDistributionTotal = "total" + AttributeDistributionToProvider = "provider_amount" AttributeConsumerRewardDenom = "consumer_reward_denom" ) From 4cb6283c7b4df68242bcce479aff1631c065a324 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Fri, 22 Sep 2023 13:18:48 +0200 Subject: [PATCH 19/29] fix linter issue --- x/ccv/provider/keeper/punish_validator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index c31f0f03a7..9286c25823 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -234,7 +234,7 @@ func TestComputePowerToSlash(t *testing.T) { []stakingtypes.Redelegation{}, int64(3000), sdk.NewInt(5), - int64(0/5 + 3000), + int64(3000), // expectedPower is 0/5 + 3000 }, { "no undelegations", From 6f15a61ebbbe97ea02d4262762ddcb0c6b74628c Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Fri, 22 Sep 2023 13:25:18 +0200 Subject: [PATCH 20/29] cherry-picked ADR-05 so markdown link checker does not complain --- ...cryptographic-equivocation-verification.md | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 docs/docs/adrs/adr-005-cryptographic-equivocation-verification.md diff --git a/docs/docs/adrs/adr-005-cryptographic-equivocation-verification.md b/docs/docs/adrs/adr-005-cryptographic-equivocation-verification.md new file mode 100644 index 0000000000..dac41b912e --- /dev/null +++ b/docs/docs/adrs/adr-005-cryptographic-equivocation-verification.md @@ -0,0 +1,105 @@ +--- +sidebar_position: 4 +title: Cryptographic verification of equivocation evidence +--- +# ADR 005: Cryptographic verification of equivocation evidence + +## Changelog +* 5/1/2023: First draft +* 7/23/23: Add light client attacks handling + +## Status + +Accepted + +## Context + +Currently, we use a governance proposal to slash validators for equivocation (double signing and light client attacks). +Every proposal needs to go through a (two weeks) voting period before it can be approved. +Given a three-week unbonding period, this means that an equivocation proposal needs to be submitted within one week since the infraction occurred. + +This ADR proposes a system to slash validators automatically for equivocation, immediately upon the provider chain's receipt of the evidence. Another thing to note is that we intend to introduce this system in stages, since even the partial ability to slash and/or tombstone is a strict improvement in security. +For the first stage of this work, we will only handle light client attacks. + +### Light Client Attack + +In a nutshell, the light client is a process that solely verifies a specific state machine's +consensus without executing the transactions. The light clients get new headers by querying +multiple nodes, called primary and witness nodes. + +Light clients download new headers committed on chain from a primary. Headers can be verified in two ways: sequentially, +where the block height of headers is serial, or using skipping. This second verification method allows light clients to download headers +with nonconsecutive block height, where some intermediate headers are skipped (see [Tendermint Light Client, Figure 1 and Figure 3](https://arxiv.org/pdf/2010.07031.pdf)). +Additionally, light clients are cross-checking new headers obtained from a primary with witnesses to ensure all nodes share the same state. + +A light client attack occurs when a Byzantine validator sends invalid headers to a light client. +As the light client doesn't execute transactions, it can be deceived into trusting corrupted application state transitions. +For instance, if a light client receives header `A` from the primary and header `B` from a witness for the same block height `H`, +and both headers are successfully verified, it indicates a light client attack. +Note that in this case, either the primary or the witness or both are malicious. + +The types of light client attacks are defined by analyzing the differences between the conflicting headers. +There are three types of light client attacks: lunatic attack, equivocation attack, and amnesia attack. +For details, see the [CometBFT specification](https://github.com/cometbft/cometbft/blob/main/spec/light-client/attacks/notes-on-evidence-handling.md#evidence-handling). + +When a light client agent detects two conflicting headers, it will initially verify their traces (see [cometBFT detector](https://github.com/cometbft/cometbft/blob/2af25aea6cfe6ac4ddac40ceddfb8c8eee17d0e6/light/detector.go#L28)) using its primary and witness nodes. +If these headers pass successful verification, the Byzantine validators will be identified based on the header's commit signatures +and the type of light client attack. The agent will then transmit this information to its nodes using a [`LightClientAttackEvidence`](https://github.com/cometbft/cometbft/blob/feed0ddf564e113a840c4678505601256b93a8bc/docs/architecture/adr-047-handling-evidence-from-light-client.md) to be eventually voted on and added to a block. +Note that from a light client agent perspective, it is not possible to establish whether a primary or a witness node, or both, are malicious. +Therefore, it will create and send two `LightClientAttackEvidence`: one against the primary (sent to the witness), and one against the witness (sent to the primary). +Both nodes will then verify it before broadcasting it and adding it to the [evidence pool](https://github.com/cometbft/cometbft/blob/2af25aea6cfe6ac4ddac40ceddfb8c8eee17d0e6/evidence/pool.go#L28). +If a `LightClientAttackEvidence` is finally committed to a block, the chain's evidence module will execute it, resulting in the jailing and the slashing of the validators responsible for the light client attack. + + +Light clients are a core component of IBC. In the event of a light client attack, IBC relayers notify the affected chains by submitting an [IBC misbehavior message](https://github.com/cosmos/ibc-go/blob/2b7c969066fbcb18f90c7f5bd256439ca12535c7/proto/ibc/lightclients/tendermint/v1/tendermint.proto#L79). +A misbehavior message includes the conflicting headers that constitute a `LightClientAttackEvidence`. Upon receiving such a message, +a chain will first verify whether these headers would have convinced its light client. This verification is achieved by checking +the header states against the light client consensus states (see [IBC misbehaviour handler](https://github.com/cosmos/ibc-go/blob/2b7c969066fbcb18f90c7f5bd256439ca12535c7/modules/light-clients/07-tendermint/types/misbehaviour_handle.go#L101)). If the misbehaviour is successfully verified, the chain will then "freeze" the +light client, halting any further trust in or updating of its states. + + +## Decision + +In the first iteration of the feature, we will introduce a new endpoint: `HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmtypes.Misbehaviour)`. +The main idea is to leverage the current IBC misbehaviour handling and update it to solely jail and slash the validators that +performed a light client attack. This update will be made under the assumption that the chain connected via this light client +share the same validator set, as it is the case with Replicated Security. + +This endpoint will reuse the IBC client libraries to verify that the misbehaviour headers would have fooled the light client. +Additionally, it’s crucial that the endpoint logic result in the slashing and jailing of validators under the same conditions +as a light client agent detector. Therefore, the endpoint will ensure that the two conditions are met: +the headers in the misbehaviour message have the same block height, and +the light client isn’t expired. + +After having successfully verified a misbehaviour, the endpoint will execute the jailing and slashing of the malicious validators similarly as in the evidence module. + +### Current limitations: + +- This only handles light client attacks, not double signing. In the future, we will add the code to also verify double signing. + +- We cannot derive an infraction height from the evidence, so it is only possible to tombstone validators, not actually slash them. +To explain the technical reasons behind this limitation, let's recap the initial consumer initiated slashing logic. +In a nutshell, consumer heights are mapped to provider heights through VSCPackets, namely through the so called vscIDs. +When an infraction occurs on the consumer, a SlashPacket containing the vscID obtained from mapping the consumer infraction height +is sent to the provider. Upon receiving the packet, the provider maps the consumer infraction height to a local infraction height, +which is used to slash the misbehaving validator. In the context of untrusted consumer chains, all their states, including vscIDs, +could be corrupted and therefore cannot be used for slashing purposes. + +- Currently, the endpoint can only handle "equivocation" light client attacks. This is because the "lunatic" attacks require the endpoint to possess the ability to dissociate which header is conflicted or trusted upon receiving a misbehavior message. Without this information, it's not possible to define the Byzantine validators from the conflicting headers (see [comment](https://github.com/cosmos/interchain-security/pull/826#discussion_r1268668684)). + + +## Consequences + +### Positive + +- After this ADR is applied, it will be possible for the provider chain to tombstone validators who committed a light client attack. + +### Negative + +- N/A + + +## References + +* [ICS misbehaviour handling PR](https://github.com/cosmos/interchain-security/pull/826) +* [Architectural diagrams](https://docs.google.com/document/d/1fe1uSJl1ZIYWXoME3Yf4Aodvz7V597Ric875JH-rigM/edit#heading=h.rv4t8i6d6jfn) From 93c2cdb07ceede8294c68f72a6c6bff4b20e4900 Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Fri, 22 Sep 2023 14:43:42 +0200 Subject: [PATCH 21/29] lint --- x/ccv/provider/keeper/double_vote.go | 2 +- x/ccv/provider/keeper/punish_validator.go | 7 +++--- .../provider/keeper/punish_validator_test.go | 23 ++++++++++++------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go index bb48767bd6..1cf7e0fc56 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -3,8 +3,8 @@ package keeper import ( "bytes" "fmt" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + 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" diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index 84e3511c56..755d39e055 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -1,12 +1,13 @@ package keeper import ( + "time" + 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" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" - "time" ) // JailAndTombstoneValidator jails and tombstones the validator with the given provider consensus address @@ -46,8 +47,8 @@ func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.Pr // ComputePowerToSlash computes the power to be slashed based on the tokens in non-matured (based on the // provider `now` time) `undelegations` and `redelegations`, as well as the current `power` of the validator func (k Keeper) ComputePowerToSlash(now time.Time, undelegations []stakingtypes.UnbondingDelegation, - redelegations []stakingtypes.Redelegation, power int64, powerReduction sdk.Int) int64 { - + redelegations []stakingtypes.Redelegation, power int64, powerReduction sdk.Int, +) int64 { // compute the total numbers of tokens currently being undelegated undelegationsInTokens := sdk.NewInt(0) for _, u := range undelegations { diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index 9286c25823..2d7608b83b 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -2,6 +2,9 @@ package keeper_test import ( "fmt" + "testing" + "time" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" @@ -15,8 +18,6 @@ import ( "github.com/stretchr/testify/require" tmtypes "github.com/tendermint/tendermint/types" - "testing" - "time" ) // TestJailAndTombstoneValidator tests that the jailing of a validator is only executed @@ -196,7 +197,8 @@ func TestComputePowerToSlash(t *testing.T) { // 1000 total undelegation tokens []stakingtypes.UnbondingDelegation{ createUndelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, nowPlus1Hour}), - createUndelegation([]int64{500}, []time.Time{nowPlus1Hour, nowPlus1Hour})}, + createUndelegation([]int64{500}, []time.Time{nowPlus1Hour, nowPlus1Hour}), + }, // 1000 total redelegation tokens []stakingtypes.Redelegation{ createRedelegation([]int64{500}, []time.Time{nowPlus1Hour, nowPlus1Hour}), @@ -214,7 +216,8 @@ func TestComputePowerToSlash(t *testing.T) { createUndelegation([]int64{}, []time.Time{}), createUndelegation([]int64{100, 100}, []time.Time{nowPlus1Hour, nowPlus1Hour}), createUndelegation([]int64{800}, []time.Time{nowPlus1Hour}), - createUndelegation([]int64{500}, []time.Time{nowPlus1Hour})}, + createUndelegation([]int64{500}, []time.Time{nowPlus1Hour}), + }, // 3500 total redelegation tokens []stakingtypes.Redelegation{ createRedelegation([]int64{}, []time.Time{}), @@ -260,7 +263,8 @@ func TestComputePowerToSlash(t *testing.T) { createUndelegation([]int64{}, []time.Time{}), createUndelegation([]int64{100, 100}, []time.Time{nowPlus1Hour, nowPlus1Hour}), createUndelegation([]int64{800}, []time.Time{nowPlus1Hour}), - createUndelegation([]int64{500}, []time.Time{nowPlus1Hour})}, + createUndelegation([]int64{500}, []time.Time{nowPlus1Hour}), + }, []stakingtypes.Redelegation{}, int64(1), sdk.NewInt(3), @@ -275,7 +279,8 @@ func TestComputePowerToSlash(t *testing.T) { createUndelegation([]int64{}, []time.Time{}), createUndelegation([]int64{100, 100}, []time.Time{now, nowPlus1Hour}), createUndelegation([]int64{800}, []time.Time{nowPlus1Hour}), - createUndelegation([]int64{500}, []time.Time{now})}, + createUndelegation([]int64{500}, []time.Time{now}), + }, // 3500 total redelegation tokens, 350 + 200 + 400 = 950 of those are from mature redelegations // so 3500 - 950 = 2550 []stakingtypes.Redelegation{ @@ -344,10 +349,12 @@ func TestSlashValidator(t *testing.T) { // we also create 1000 tokens worth of redelegations, 750 of them are non-matured undelegations := []stakingtypes.UnbondingDelegation{ createUndelegation([]int64{250, 250}, []time.Time{nowPlus1Hour, now}), - createUndelegation([]int64{500}, []time.Time{nowPlus1Hour})} + createUndelegation([]int64{500}, []time.Time{nowPlus1Hour}), + } redelegations := []stakingtypes.Redelegation{ createRedelegation([]int64{250, 250}, []time.Time{now, nowPlus1Hour}), - createRedelegation([]int64{500}, []time.Time{nowPlus1Hour})} + createRedelegation([]int64{500}, []time.Time{nowPlus1Hour}), + } // validator's current power currentPower := int64(3000) From 1d3ee78273b5a16372b04598148330253b901b1a Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Mon, 25 Sep 2023 16:33:29 +0200 Subject: [PATCH 22/29] Use cached context to get tokens in undelegations and redelegations. --- testutil/keeper/mocks.go | 28 +++++++++ x/ccv/provider/keeper/punish_validator.go | 30 +++------ .../provider/keeper/punish_validator_test.go | 61 ++++++++++++++++++- x/ccv/types/expected_keepers.go | 2 + 4 files changed, 99 insertions(+), 22 deletions(-) diff --git a/testutil/keeper/mocks.go b/testutil/keeper/mocks.go index 3df4110286..dffbc1ca09 100644 --- a/testutil/keeper/mocks.go +++ b/testutil/keeper/mocks.go @@ -307,6 +307,34 @@ func (mr *MockStakingKeeperMockRecorder) Slash(arg0, arg1, arg2, arg3, arg4, arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Slash", reflect.TypeOf((*MockStakingKeeper)(nil).Slash), arg0, arg1, arg2, arg3, arg4, arg5) } +// SlashRedelegation mocks base method. +func (m *MockStakingKeeper) SlashRedelegation(arg0 types.Context, arg1 types4.Validator, arg2 types4.Redelegation, arg3 int64, arg4 types.Dec) types.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SlashRedelegation", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(types.Int) + return ret0 +} + +// SlashRedelegation indicates an expected call of SlashRedelegation. +func (mr *MockStakingKeeperMockRecorder) SlashRedelegation(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SlashRedelegation", reflect.TypeOf((*MockStakingKeeper)(nil).SlashRedelegation), arg0, arg1, arg2, arg3, arg4) +} + +// SlashUnbondingDelegation mocks base method. +func (m *MockStakingKeeper) SlashUnbondingDelegation(arg0 types.Context, arg1 types4.UnbondingDelegation, arg2 int64, arg3 types.Dec) types.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SlashUnbondingDelegation", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(types.Int) + return ret0 +} + +// SlashUnbondingDelegation indicates an expected call of SlashUnbondingDelegation. +func (mr *MockStakingKeeperMockRecorder) SlashUnbondingDelegation(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SlashUnbondingDelegation", reflect.TypeOf((*MockStakingKeeper)(nil).SlashUnbondingDelegation), arg0, arg1, arg2, arg3) +} + // UnbondingCanComplete mocks base method. func (m *MockStakingKeeper) UnbondingCanComplete(ctx types.Context, id uint64) error { m.ctrl.T.Helper() diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index 755d39e055..3fc67afecb 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -1,8 +1,6 @@ package keeper import ( - "time" - sdk "github.com/cosmos/cosmos-sdk/types" evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" @@ -44,33 +42,25 @@ func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.Pr k.slashingKeeper.Tombstone(ctx, providerAddr.ToSdkConsAddr()) } -// ComputePowerToSlash computes the power to be slashed based on the tokens in non-matured (based on the -// provider `now` time) `undelegations` and `redelegations`, as well as the current `power` of the validator -func (k Keeper) ComputePowerToSlash(now time.Time, undelegations []stakingtypes.UnbondingDelegation, +// ComputePowerToSlash computes the power to be slashed based on the tokens in non-matured `undelegations` and +// `redelegations`, as well as the current `power` of the validator +func (k Keeper) ComputePowerToSlash(ctx sdk.Context, validator stakingtypes.Validator, undelegations []stakingtypes.UnbondingDelegation, redelegations []stakingtypes.Redelegation, power int64, powerReduction sdk.Int, ) int64 { // compute the total numbers of tokens currently being undelegated undelegationsInTokens := sdk.NewInt(0) + + cachedCtx, _ := ctx.CacheContext() for _, u := range undelegations { - for _, entry := range u.Entries { - if entry.IsMature(now) && !entry.OnHold() { - // undelegation no longer eligible for slashing, skip it - continue - } - undelegationsInTokens = undelegationsInTokens.Add(entry.InitialBalance) - } + amountSlashed := k.stakingKeeper.SlashUnbondingDelegation(cachedCtx, u, 0, sdk.NewDec(1)) + undelegationsInTokens = undelegationsInTokens.Add(amountSlashed) } // compute the total numbers of tokens currently being redelegated redelegationsInTokens := sdk.NewInt(0) for _, r := range redelegations { - for _, entry := range r.Entries { - if entry.IsMature(now) && !entry.OnHold() { - // redelegation no longer eligible for slashing, skip it - continue - } - redelegationsInTokens = redelegationsInTokens.Add(entry.InitialBalance) - } + amountSlashed := k.stakingKeeper.SlashRedelegation(cachedCtx, validator, r, 0, sdk.NewDec(1)) + redelegationsInTokens = redelegationsInTokens.Add(amountSlashed) } // The power we pass to staking's keeper `Slash` method is the current power of the validator together with the total @@ -105,7 +95,7 @@ func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsA redelegations := k.stakingKeeper.GetRedelegationsFromSrcValidator(ctx, validator.GetOperator()) lastPower := k.stakingKeeper.GetLastValidatorPower(ctx, validator.GetOperator()) powerReduction := k.stakingKeeper.PowerReduction(ctx) - totalPower := k.ComputePowerToSlash(ctx.BlockHeader().Time, undelegations, redelegations, lastPower, powerReduction) + totalPower := k.ComputePowerToSlash(ctx, validator, undelegations, redelegations, lastPower, powerReduction) slashFraction := k.slashingKeeper.SlashFractionDoubleSign(ctx) k.stakingKeeper.Slash(ctx, providerAddr.ToSdkConsAddr(), 0, totalPower, slashFraction, stakingtypes.DoubleSign) diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index 2d7608b83b..1166fbf063 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -176,7 +176,7 @@ func createRedelegation(initialBalances []int64, completionTimes []time.Time) st // TestComputePowerToSlash tests that `ComputePowerToSlash` computes the correct power to be slashed based on // the tokens in non-mature undelegation and redelegation entries, as well as the current power of the validator func TestComputePowerToSlash(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() // undelegation or redelegation entries with completion time `now` have matured @@ -297,8 +297,39 @@ func TestComputePowerToSlash(t *testing.T) { }, } + pubKey, _ := cryptocodec.FromTmPubKeyInterface(tmtypes.NewMockPV().PrivKey.PubKey()) + validator, _ := stakingtypes.NewValidator(pubKey.Address().Bytes(), pubKey, stakingtypes.Description{}) + for _, tc := range testCases { - actualPower := providerKeeper.ComputePowerToSlash(now, + gomock.InOrder(mocks.MockStakingKeeper.EXPECT(). + SlashUnbondingDelegation(gomock.Any(), gomock.Any(), int64(0), sdk.NewDec(1)). + DoAndReturn( + func(_ sdk.Context, undelegation stakingtypes.UnbondingDelegation, _ int64, _ sdk.Dec) sdk.Int { + sum := sdk.NewInt(0) + for _, r := range undelegation.Entries { + if r.IsMature(ctx.BlockTime()) { + continue + } + sum = sum.Add(sdk.NewInt(r.InitialBalance.Int64())) + } + return sum + }).AnyTimes(), + mocks.MockStakingKeeper.EXPECT(). + SlashRedelegation(gomock.Any(), gomock.Any(), gomock.Any(), int64(0), sdk.NewDec(1)). + DoAndReturn( + func(ctx sdk.Context, _ stakingtypes.Validator, redelegation stakingtypes.Redelegation, _ int64, _ sdk.Dec) sdk.Int { + sum := sdk.NewInt(0) + for _, r := range redelegation.Entries { + if r.IsMature(ctx.BlockTime()) { + continue + } + sum = sum.Add(sdk.NewInt(r.InitialBalance.Int64())) + } + return sum + }).AnyTimes(), + ) + + actualPower := providerKeeper.ComputePowerToSlash(ctx, validator, tc.undelegations, tc.redelegations, tc.power, tc.powerReduction) if tc.expectedPower != actualPower { @@ -386,6 +417,32 @@ func TestSlashValidator(t *testing.T) { mocks.MockStakingKeeper.EXPECT(). PowerReduction(ctx). Return(powerReduction), + mocks.MockStakingKeeper.EXPECT(). + SlashUnbondingDelegation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(_ sdk.Context, undelegation stakingtypes.UnbondingDelegation, _ int64, _ sdk.Dec) sdk.Int { + sum := sdk.NewInt(0) + for _, r := range undelegation.Entries { + if r.IsMature(ctx.BlockTime()) { + continue + } + sum = sum.Add(sdk.NewInt(r.InitialBalance.Int64())) + } + return sum + }).AnyTimes(), + mocks.MockStakingKeeper.EXPECT(). + SlashRedelegation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(_ sdk.Context, _ stakingtypes.Validator, redelegation stakingtypes.Redelegation, _ int64, _ sdk.Dec) sdk.Int { + sum := sdk.NewInt(0) + for _, r := range redelegation.Entries { + if r.IsMature(ctx.BlockTime()) { + continue + } + sum = sum.Add(sdk.NewInt(r.InitialBalance.Int64())) + } + return sum + }).AnyTimes(), mocks.MockSlashingKeeper.EXPECT(). SlashFractionDoubleSign(ctx). Return(slashFraction), diff --git a/x/ccv/types/expected_keepers.go b/x/ccv/types/expected_keepers.go index e3505c6bb7..1aa2b6dc8f 100644 --- a/x/ccv/types/expected_keepers.go +++ b/x/ccv/types/expected_keepers.go @@ -30,6 +30,8 @@ type StakingKeeper interface { // slash the validator and delegators of the validator, specifying offence height, offence power, and slash fraction Jail(sdk.Context, sdk.ConsAddress) // jail a validator Slash(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec, stakingtypes.InfractionType) + SlashUnbondingDelegation(sdk.Context, stakingtypes.UnbondingDelegation, int64, sdk.Dec) sdk.Int + SlashRedelegation(sdk.Context, stakingtypes.Validator, stakingtypes.Redelegation, int64, sdk.Dec) sdk.Int Unjail(ctx sdk.Context, addr sdk.ConsAddress) GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, found bool) IterateLastValidatorPowers(ctx sdk.Context, cb func(addr sdk.ValAddress, power int64) (stop bool)) From 5c053bb8ffc251e5e2c245d205f8e8dfd6a5e3b9 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Tue, 26 Sep 2023 10:15:10 +0200 Subject: [PATCH 23/29] return the error --- x/ccv/provider/keeper/double_vote.go | 8 +++-- x/ccv/provider/keeper/misbehaviour.go | 15 ++++++-- x/ccv/provider/keeper/punish_validator.go | 42 +++++++++++------------ 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/x/ccv/provider/keeper/double_vote.go b/x/ccv/provider/keeper/double_vote.go index 1cf7e0fc56..d3813f8da4 100644 --- a/x/ccv/provider/keeper/double_vote.go +++ b/x/ccv/provider/keeper/double_vote.go @@ -32,8 +32,12 @@ func (k Keeper) HandleConsumerDoubleVoting( types.NewConsumerConsAddress(sdk.ConsAddress(evidence.VoteA.ValidatorAddress.Bytes())), ) - k.SlashValidator(ctx, providerAddr) - k.JailAndTombstoneValidator(ctx, providerAddr) + if err := k.SlashValidator(ctx, providerAddr); err != nil { + return err + } + if err := k.JailAndTombstoneValidator(ctx, providerAddr); err != nil { + return err + } k.Logger(ctx).Info( "confirmed equivocation", diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 980562be44..e35e04db1d 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -1,6 +1,7 @@ package keeper import ( + "fmt" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -34,6 +35,7 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty provAddrs := make([]types.ProviderConsAddress, len(byzantineValidators)) + var errors []error // slash, jail, and tombstone the Byzantine validators for _, v := range byzantineValidators { providerAddr := k.GetProviderAddrFromConsumerAddr( @@ -41,8 +43,14 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty misbehaviour.Header1.Header.ChainID, types.NewConsumerConsAddress(sdk.ConsAddress(v.Address.Bytes())), ) - k.SlashValidator(ctx, providerAddr) - k.JailAndTombstoneValidator(ctx, providerAddr) + err := k.SlashValidator(ctx, providerAddr) + if err != nil { + errors = append(errors, err) + } + err = k.JailAndTombstoneValidator(ctx, providerAddr) + if err != nil { + errors = append(errors, err) + } provAddrs = append(provAddrs, providerAddr) } @@ -51,6 +59,9 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty "byzantine validators", provAddrs, ) + if len(errors) > 0 { + return fmt.Errorf("failed to slash, jail, or tombstone validators: %v", errors) + } return nil } diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index 3fc67afecb..06a7a30da2 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -1,32 +1,32 @@ package keeper import ( + errorsmod "cosmossdk.io/errors" + "fmt" sdk "github.com/cosmos/cosmos-sdk/types" evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" - + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" ) // JailAndTombstoneValidator jails and tombstones the validator with the given provider consensus address -func (k Keeper) JailAndTombstoneValidator(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", "provider consensus address", providerAddr.String()) - return +func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) error { + validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) + if !found { + return errorsmod.Wrapf(slashingtypes.ErrNoValidatorForAddress, "provider consensus address: %s", providerAddr.String()) + } + + if validator.IsUnbonded() { + return fmt.Errorf("validator is unbonded. provider consensus address: %s", providerAddr.String()) } - // check that the validator isn't tombstoned if k.slashingKeeper.IsTombstoned(ctx, providerAddr.ToSdkConsAddr()) { - logger.Info("validator is already tombstoned", "provider consensus address", providerAddr.String()) - return + return fmt.Errorf("validator is tombstoned. provider consensus address: %s", providerAddr.String()) } // jail validator if not already - if !val.IsJailed() { + if !validator.IsJailed() { k.stakingKeeper.Jail(ctx, providerAddr.ToSdkConsAddr()) } @@ -40,6 +40,8 @@ func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.Pr // because then a validator could i) perform an equivocation, ii) get jailed (e.g., through downtime) // and in such a case the validator would not get slashed when we call `SlashValidator`. k.slashingKeeper.Tombstone(ctx, providerAddr.ToSdkConsAddr()) + + return nil } // ComputePowerToSlash computes the power to be slashed based on the tokens in non-matured `undelegations` and @@ -72,23 +74,18 @@ func (k Keeper) ComputePowerToSlash(ctx sdk.Context, validator stakingtypes.Vali } // SlashValidator slashes validator with `providerAddr` -func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) { - logger := k.Logger(ctx) - +func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) error { validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) if !found { - logger.Error("validator not found", "provider consensus address", providerAddr.String()) - return + return errorsmod.Wrapf(slashingtypes.ErrNoValidatorForAddress, "provider consensus address: %s", providerAddr.String()) } if validator.IsUnbonded() { - logger.Info("validator is unbonded", "provider consensus address", providerAddr.String()) - return + return fmt.Errorf("validator is unbonded. provider consensus address: %s", providerAddr.String()) } if k.slashingKeeper.IsTombstoned(ctx, providerAddr.ToSdkConsAddr()) { - logger.Info("validator is already tombstoned", "provider consensus address", providerAddr.String()) - return + return fmt.Errorf("validator is tombstoned. provider consensus address: %s", providerAddr.String()) } undelegations := k.stakingKeeper.GetUnbondingDelegationsFromValidator(ctx, validator.GetOperator()) @@ -99,4 +96,5 @@ func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsA slashFraction := k.slashingKeeper.SlashFractionDoubleSign(ctx) k.stakingKeeper.Slash(ctx, providerAddr.ToSdkConsAddr(), 0, totalPower, slashFraction, stakingtypes.DoubleSign) + return nil } From f8633b5612ef17519a7ef893441193dcaa9ef9aa Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Tue, 26 Sep 2023 10:19:59 +0200 Subject: [PATCH 24/29] lint issue --- x/ccv/provider/keeper/misbehaviour.go | 1 + x/ccv/provider/keeper/punish_validator.go | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index e35e04db1d..497767f84b 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -2,6 +2,7 @@ package keeper import ( "fmt" + "github.com/cosmos/interchain-security/v2/x/ccv/provider/types" sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index 06a7a30da2..eac18b4d39 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -1,8 +1,10 @@ package keeper import ( - errorsmod "cosmossdk.io/errors" "fmt" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" From f96f1bce5d5f1a31e3f5d90795b44669e8a10c0b Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Tue, 26 Sep 2023 11:51:33 +0200 Subject: [PATCH 25/29] take into account Philip's comments --- tests/integration/double_vote.go | 6 +++--- x/ccv/provider/keeper/punish_validator.go | 4 +++- x/ccv/provider/keeper/punish_validator_test.go | 8 ++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index 778d98080a..3f7120edaf 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -127,7 +127,7 @@ 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 1 - should pass", + "valid double voting evidence - should pass", &tmtypes.DuplicateVoteEvidence{ VoteA: consuVote, VoteB: consuBadVote, @@ -141,7 +141,7 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { }, { // create a double voting evidence using the provider validator key - "valid double voting evidence 2 - should pass", + "valid double voting evidence - should not pass because validator tombstoned in the previous test case", &tmtypes.DuplicateVoteEvidence{ VoteA: provVote, VoteB: provBadVote, @@ -151,7 +151,7 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { }, s.consumerChain.ChainID, provVal.PubKey, - true, + false, }, } diff --git a/x/ccv/provider/keeper/punish_validator.go b/x/ccv/provider/keeper/punish_validator.go index eac18b4d39..6c9e88f260 100644 --- a/x/ccv/provider/keeper/punish_validator.go +++ b/x/ccv/provider/keeper/punish_validator.go @@ -47,13 +47,15 @@ func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.Pr } // ComputePowerToSlash computes the power to be slashed based on the tokens in non-matured `undelegations` and -// `redelegations`, as well as the current `power` of the validator +// `redelegations`, as well as the current `power` of the validator. +// Note that this method does not perform any slashing. func (k Keeper) ComputePowerToSlash(ctx sdk.Context, validator stakingtypes.Validator, undelegations []stakingtypes.UnbondingDelegation, redelegations []stakingtypes.Redelegation, power int64, powerReduction sdk.Int, ) int64 { // compute the total numbers of tokens currently being undelegated undelegationsInTokens := sdk.NewInt(0) + // Note that we use a **cached** context to avoid any actual slashing of undelegations or redelegations. cachedCtx, _ := ctx.CacheContext() for _, u := range undelegations { amountSlashed := k.stakingKeeper.SlashUnbondingDelegation(cachedCtx, u, 0, sdk.NewDec(1)) diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index 1166fbf063..b3ed24a744 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -329,9 +329,17 @@ func TestComputePowerToSlash(t *testing.T) { }).AnyTimes(), ) + tokensBeforeCall := validator.GetTokens() + delegatorSharesBeforeCall := validator.GetDelegatorShares() + actualPower := providerKeeper.ComputePowerToSlash(ctx, validator, tc.undelegations, tc.redelegations, tc.power, tc.powerReduction) + // safeguard check that validator remains unmodified after a call to `ComputePowerToSlash` + // `ComputePowerToSlash` only computes the power and does not modify the state of the system in any way + require.Equal(t, tokensBeforeCall, validator.GetTokens()) + require.Equal(t, delegatorSharesBeforeCall, validator.GetDelegatorShares()) + if tc.expectedPower != actualPower { require.Fail(t, fmt.Sprintf("\"%s\" failed", tc.name), "expected is %d but actual is %d", tc.expectedPower, actualPower) From d3dc0bc8fd30c23d8d93b405b829a11c15eaa6ad Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Tue, 26 Sep 2023 15:03:27 +0200 Subject: [PATCH 26/29] clean up --- tests/integration/double_vote.go | 12 ++++++------ x/ccv/provider/keeper/punish_validator_test.go | 8 -------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index 3f7120edaf..f22338301c 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -127,7 +127,7 @@ 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: consuVote, VoteB: consuBadVote, @@ -141,7 +141,7 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { }, { // create a double voting evidence using the provider validator key - "valid double voting evidence - should not pass because validator tombstoned in the previous test case", + "valid double voting evidence 2 - should pass", &tmtypes.DuplicateVoteEvidence{ VoteA: provVote, VoteB: provBadVote, @@ -151,7 +151,7 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { }, s.consumerChain.ChainID, provVal.PubKey, - false, + true, }, } @@ -190,10 +190,10 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { s.Require().True(s.providerApp.GetTestStakingKeeper().IsValidatorJailed(provCtx, provAddr.ToSdkConsAddr())) s.Require().True(s.providerApp.GetTestSlashingKeeper().IsTombstoned(provCtx, provAddr.ToSdkConsAddr())) - // verifies that the validator gets slashed and has fewer tokens after the slashing - validator, _ := s.providerApp.GetTestStakingKeeper().GetValidator(provCtx, provAddr.ToSdkConsAddr().Bytes()) + // verifies that the val gets slashed and has fewer tokens after the slashing + val, _ := s.providerApp.GetTestStakingKeeper().GetValidator(provCtx, provAddr.ToSdkConsAddr().Bytes()) slashFraction := s.providerApp.GetTestSlashingKeeper().SlashFractionDoubleSign(provCtx) - actualTokens := validator.GetTokens().ToDec() + actualTokens := val.GetTokens().ToDec() s.Require().True(initialTokens.Sub(initialTokens.Mul(slashFraction)).Equal(actualTokens)) } else { s.Require().Error(err) diff --git a/x/ccv/provider/keeper/punish_validator_test.go b/x/ccv/provider/keeper/punish_validator_test.go index b3ed24a744..1166fbf063 100644 --- a/x/ccv/provider/keeper/punish_validator_test.go +++ b/x/ccv/provider/keeper/punish_validator_test.go @@ -329,17 +329,9 @@ func TestComputePowerToSlash(t *testing.T) { }).AnyTimes(), ) - tokensBeforeCall := validator.GetTokens() - delegatorSharesBeforeCall := validator.GetDelegatorShares() - actualPower := providerKeeper.ComputePowerToSlash(ctx, validator, tc.undelegations, tc.redelegations, tc.power, tc.powerReduction) - // safeguard check that validator remains unmodified after a call to `ComputePowerToSlash` - // `ComputePowerToSlash` only computes the power and does not modify the state of the system in any way - require.Equal(t, tokensBeforeCall, validator.GetTokens()) - require.Equal(t, delegatorSharesBeforeCall, validator.GetDelegatorShares()) - if tc.expectedPower != actualPower { require.Fail(t, fmt.Sprintf("\"%s\" failed", tc.name), "expected is %d but actual is %d", tc.expectedPower, actualPower) From 39bbcf5fb1c391c48f015867cf7cc3ac0adc8d30 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Wed, 27 Sep 2023 10:21:44 +0200 Subject: [PATCH 27/29] fix flakey test --- tests/integration/double_vote.go | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index f22338301c..5ce0cc418c 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -1,6 +1,7 @@ package integration import ( + "bytes" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdk "github.com/cosmos/cosmos-sdk/types" testutil "github.com/cosmos/interchain-security/v2/testutil/crypto" @@ -28,7 +29,36 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { provValSet, err := tmtypes.ValidatorSetFromProto(s.providerChain.LastHeader.ValidatorSet) s.Require().NoError(err) - provVal := provValSet.Validators[0] + + // In what follows, we have the "valid double voting evidence 1 - should pass," and + // "valid double voting evidence 2 - should pass" test cases. The first test case has valid double + // voting evidence for the consumer chain (using a consumer key), while the second test case has valid double + // voting evidence (using a provider key). When the first "valid double voting evidence 1 - should pass" test runs + // it would slash the validator that corresponds to the consumer validator. If the second test + // "valid double voting evidence 2 - should pass" attempts to slash the same validator, the test would fail + // because the validator was already slashed in the first test case. + // Depending on what `consuSigner` and the `provSigner` are, that are arbitrarily chosen, when we run the test, we + // might have a successful or a failed test. To prevent a flaky test like this, in what follows we check that we find the + // validator that corresponds to `consuVal` and then find a `provVal` validator that is not the corresponding + // validator of `consuVal` on the provider chain. This way we guarantee that the test is not flaky. + allValidators := s.providerApp.GetProviderKeeper().GetAllValidatorsByConsumerAddr(s.providerCtx(), &s.consumerChain.ChainID) + consuValIndex := 0 + for i := 0; i < len(allValidators); i++ { + if bytes.Equal(allValidators[i].ConsumerAddr, consuVal.Address.Bytes()) { + consuValIndex = i + break + } + } + + provValIndex := 0 + for i := 0; i < len(provValSet.Validators); i++ { + if !bytes.Equal(allValidators[consuValIndex].ProviderAddr, provValSet.Validators[i].Address.Bytes()) { + provValIndex = i + break + } + } + + provVal := provValSet.Validators[provValIndex] provSigner := s.providerChain.Signers[provVal.Address.String()] blockID1 := testutil.MakeBlockID([]byte("blockhash"), 1000, []byte("partshash")) From 694dce76fe28aaf2b6bc1383acd5d77bee9bb9f1 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Wed, 27 Sep 2023 10:24:46 +0200 Subject: [PATCH 28/29] lint issue --- tests/integration/double_vote.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index 5ce0cc418c..9320162318 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -2,6 +2,7 @@ package integration import ( "bytes" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdk "github.com/cosmos/cosmos-sdk/types" testutil "github.com/cosmos/interchain-security/v2/testutil/crypto" From 366e3b128d39e4f478c33dd069702381c7b883f2 Mon Sep 17 00:00:00 2001 From: Karolos Antoniadis Date: Wed, 27 Sep 2023 12:00:25 +0200 Subject: [PATCH 29/29] fix error returns and fix flaky test in a better way --- tests/integration/double_vote.go | 45 +++++---------------------- x/ccv/provider/keeper/misbehaviour.go | 9 +++++- 2 files changed, 16 insertions(+), 38 deletions(-) diff --git a/tests/integration/double_vote.go b/tests/integration/double_vote.go index 9320162318..d2a00e583b 100644 --- a/tests/integration/double_vote.go +++ b/tests/integration/double_vote.go @@ -1,8 +1,6 @@ package integration import ( - "bytes" - cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdk "github.com/cosmos/cosmos-sdk/types" testutil "github.com/cosmos/interchain-security/v2/testutil/crypto" @@ -31,35 +29,7 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { provValSet, err := tmtypes.ValidatorSetFromProto(s.providerChain.LastHeader.ValidatorSet) s.Require().NoError(err) - // In what follows, we have the "valid double voting evidence 1 - should pass," and - // "valid double voting evidence 2 - should pass" test cases. The first test case has valid double - // voting evidence for the consumer chain (using a consumer key), while the second test case has valid double - // voting evidence (using a provider key). When the first "valid double voting evidence 1 - should pass" test runs - // it would slash the validator that corresponds to the consumer validator. If the second test - // "valid double voting evidence 2 - should pass" attempts to slash the same validator, the test would fail - // because the validator was already slashed in the first test case. - // Depending on what `consuSigner` and the `provSigner` are, that are arbitrarily chosen, when we run the test, we - // might have a successful or a failed test. To prevent a flaky test like this, in what follows we check that we find the - // validator that corresponds to `consuVal` and then find a `provVal` validator that is not the corresponding - // validator of `consuVal` on the provider chain. This way we guarantee that the test is not flaky. - allValidators := s.providerApp.GetProviderKeeper().GetAllValidatorsByConsumerAddr(s.providerCtx(), &s.consumerChain.ChainID) - consuValIndex := 0 - for i := 0; i < len(allValidators); i++ { - if bytes.Equal(allValidators[i].ConsumerAddr, consuVal.Address.Bytes()) { - consuValIndex = i - break - } - } - - provValIndex := 0 - for i := 0; i < len(provValSet.Validators); i++ { - if !bytes.Equal(allValidators[consuValIndex].ProviderAddr, provValSet.Validators[i].Address.Bytes()) { - provValIndex = i - break - } - } - - provVal := provValSet.Validators[provValIndex] + provVal := provValSet.Validators[0] provSigner := s.providerChain.Signers[provVal.Address.String()] blockID1 := testutil.MakeBlockID([]byte("blockhash"), 1000, []byte("partshash")) @@ -186,15 +156,16 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() { }, } - consuAddr := types.NewConsumerConsAddress(sdk.ConsAddress(consuVal.Address.Bytes())) - provAddr := s.providerApp.GetProviderKeeper().GetProviderAddrFromConsumerAddr(s.providerCtx(), s.consumerChain.ChainID, consuAddr) - - validator, _ := s.providerApp.GetTestStakingKeeper().GetValidator(s.providerCtx(), provAddr.ToSdkConsAddr().Bytes()) - initialTokens := validator.GetTokens().ToDec() for _, tc := range testCases { s.Run(tc.name, func() { + consuAddr := types.NewConsumerConsAddress(sdk.ConsAddress(tc.ev.VoteA.ValidatorAddress.Bytes())) + provAddr := s.providerApp.GetProviderKeeper().GetProviderAddrFromConsumerAddr(s.providerCtx(), s.consumerChain.ChainID, consuAddr) + + validator, _ := s.providerApp.GetTestStakingKeeper().GetValidator(s.providerCtx(), provAddr.ToSdkConsAddr().Bytes()) + initialTokens := validator.GetTokens().ToDec() + // reset context for each run - provCtx := s.providerCtx() + provCtx, _ := s.providerCtx().CacheContext() // if the evidence was built using the validator provider address and key, // we remove the consumer key assigned to the validator otherwise diff --git a/x/ccv/provider/keeper/misbehaviour.go b/x/ccv/provider/keeper/misbehaviour.go index 497767f84b..c53e11b450 100644 --- a/x/ccv/provider/keeper/misbehaviour.go +++ b/x/ccv/provider/keeper/misbehaviour.go @@ -60,9 +60,16 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty "byzantine validators", provAddrs, ) + // If we fail to slash all validators we return an error. However, if we only fail to slash some validators + // we just log an error to avoid having the whole `MsgSubmitMisbehaviour` failing and reverting the partial slashing. + if len(errors) == len(byzantineValidators) { + return fmt.Errorf("failed to slash, jail, or tombstone all validators: %v", errors) + } + if len(errors) > 0 { - return fmt.Errorf("failed to slash, jail, or tombstone validators: %v", errors) + logger.Error("failed to slash, jail, or tombstone validators: %v", errors) } + return nil }