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