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) +}