diff --git a/tests/e2e/common.go b/tests/e2e/common.go index 2ca10a39c3..ee568c18c0 100644 --- a/tests/e2e/common.go +++ b/tests/e2e/common.go @@ -615,3 +615,38 @@ func (s *CCVTestSuite) setupValidatorPowers() { } s.Require().Equal(int64(4000), stakingKeeper.GetLastTotalPower(s.providerCtx()).Int64()) } + +// getHeightOfVSCPacketRecv returns the height of when the consumer received a VSCPacket. +// If expectedMaturityTimesLen > 0, then it's expected to find expectedMaturityTimesLen +// maturity times (i.e., VSCPakcets not yet matured). The vscID of the VSCPacket is retrieved +// from the maturity time with maturityTimesIndex. +func (s *CCVTestSuite) getHeightOfVSCPacketRecv( + bundle icstestingutils.ConsumerBundle, + expectedMaturityTimesLen int, + maturityTimesIndex int, + msgAndArgs ...interface{}, +) (height uint64) { + maturityTimes := bundle.GetKeeper().GetAllPacketMaturityTimes(bundle.GetCtx()) + if expectedMaturityTimesLen > 0 { + s.Require().Len( + maturityTimes, + expectedMaturityTimesLen, + fmt.Sprintf("unexpected number of maturity times; %s", msgAndArgs...), + ) + } + vscID := maturityTimes[maturityTimesIndex].VscId + hToVSCids := bundle.GetKeeper().GetAllHeightToValsetUpdateIDs(bundle.GetCtx()) + found := false + for _, hToVSCid := range hToVSCids { + if hToVSCid.ValsetUpdateId == vscID { + height = hToVSCid.Height + found = true + break + } + } + s.Require().True( + found, + fmt.Sprintf("cannot find height mapped to vscID; %s", msgAndArgs...), + ) + return +} diff --git a/tests/e2e/slashing.go b/tests/e2e/slashing.go index 77b5bb24fa..f9769bcf6b 100644 --- a/tests/e2e/slashing.go +++ b/tests/e2e/slashing.go @@ -10,6 +10,7 @@ import ( slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ccv "github.com/cosmos/interchain-security/x/ccv/types" + "github.com/cosmos/interchain-security/x/ccv/utils" clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types" @@ -374,106 +375,349 @@ func (suite *CCVTestSuite) TestOnRecvSlashPacketErrors() { suite.Require().Equal(uint64(1), (providerKeeper.GetThrottledPacketDataSize(ctx, consumerChainID))) } -// TestHandleSlashPacketDistribution tests the slashing of an undelegation balance -// by varying the slash packet VSC ID mapping to infraction heights -// lesser, equal or greater than the undelegation entry creation height -func (suite *CCVTestSuite) TestHandleSlashPacketDistribution() { - providerKeeper := suite.providerApp.GetProviderKeeper() - providerStakingKeeper := suite.providerApp.GetE2eStakingKeeper() - providerSlashingKeeper := suite.providerApp.GetE2eSlashingKeeper() - - // choose a validator - tmValidator := suite.providerChain.Vals.Validators[0] - valAddr, err := sdk.ValAddressFromHex(tmValidator.Address.String()) - suite.Require().NoError(err) - - validator, found := providerStakingKeeper.GetValidator(suite.providerChain.GetContext(), valAddr) - suite.Require().True(found) +// TestSlashUndelegation tests the slashing of an undelegation balance in various scenarios +func (suite *CCVTestSuite) TestSlashUndelegation() { + valIndex := 0 + bondAmt := sdk.NewInt(10000000) + halfBondAmt := bondAmt.Quo(sdk.NewInt(2)) + slashFactor := suite.providerApp.GetE2eSlashingKeeper().SlashFractionDowntime(suite.providerCtx()) + slashAmountDec := slashFactor.MulInt(halfBondAmt) + slashAmount := slashAmountDec.TruncateInt() + + var powerBeforeDelegate int64 + var powerBeforeUndelegate int64 + var powerAfterUndelegate int64 + var delegateConsumerHeight uint64 + var undelegateConsumerHeight uint64 + + consumerUnbondingPeriod := suite.consumerApp.GetConsumerKeeper().GetUnbondingPeriod(suite.consumerCtx()) + fmt.Printf("consumerUnbondingPeriod: %s\n", consumerUnbondingPeriod) + providerUnbondingPeriod := suite.providerApp.GetE2eStakingKeeper().UnbondingTime(suite.providerCtx()) + fmt.Printf("providerUnbondingPeriod: %s\n", providerUnbondingPeriod) - // unbonding operations parameters - delAddr := suite.providerChain.SenderAccount.GetAddress() - bondAmt := sdk.NewInt(1000000) - - // new delegator shares used - testShares := sdk.Dec{} - - // setup the test with a delegation, a no-op and an undelegation - setupOperations := []struct { - fn func(suite *CCVTestSuite) error + testCases := []struct { + name string + slash func(consAddr sdk.ConsAddress) + expSlashOccurred bool }{ + // infraction - delegate - undelegate - slash - mature consumer - mature provider + // TODO: this behavior is unexpected + // - neither the delegation nor undelegation should be slashed { - func(suite *CCVTestSuite) error { - testShares, err = providerStakingKeeper.Delegate(suite.providerChain.GetContext(), delAddr, bondAmt, stakingtypes.Unbonded, stakingtypes.Validator(validator), true) - return err + "infraction before delegate, detected before maturity on consumer", + func(consAddr sdk.ConsAddress) { + // increment time by half of consumer unbonding period + // for the undelegation to not reach maturity yet + incrementTime(suite, consumerUnbondingPeriod/2) + + // slash + suite.consumerApp.GetConsumerKeeper().Slash( + suite.consumerCtx(), + consAddr, + int64(delegateConsumerHeight)-1, + powerBeforeDelegate, + sdk.ZeroDec(), + stakingtypes.Downtime, + ) + + // increment time so that the unbonding period ends on the consumer + incrementTime(suite, consumerUnbondingPeriod) + + // relay all packets from consumer to provider + relayAllCommittedPackets( + suite, + suite.consumerChain, + suite.path, + ccv.ConsumerPortID, + suite.path.EndpointA.ChannelID, + 3, // 2 VSCMaturedPackets and 1 SlashPacket + ) }, - }, { - func(suite *CCVTestSuite) error { - return nil + true, // TODO: this should be false! + }, + // delegate - infraction - undelegate - slash - mature consumer - mature provider + // slash + { + "infraction after delegate, before undelegate, detected before maturity on consumer", + func(consAddr sdk.ConsAddress) { + // increment time by half of consumer unbonding period + // for the undelegation to not reach maturity yet + incrementTime(suite, consumerUnbondingPeriod/2) + + // slash + suite.consumerApp.GetConsumerKeeper().Slash( + suite.consumerCtx(), + consAddr, + int64(undelegateConsumerHeight)-1, + powerBeforeUndelegate, + sdk.ZeroDec(), + stakingtypes.Downtime, + ) + + // increment time so that the unbonding period ends on the consumer + incrementTime(suite, consumerUnbondingPeriod) + + // relay all packets from consumer to provider + relayAllCommittedPackets( + suite, + suite.consumerChain, + suite.path, + ccv.ConsumerPortID, + suite.path.EndpointA.ChannelID, + 3, // 2 VSCMaturedPackets and 1 SlashPacket + ) + }, + // infraction before undelegate; slash successful + true, + }, + // delegate - undelegate - infraction - slash - mature consumer - mature provider + // no slash + { + "infraction after undelegate, detected before maturity on consumer", + func(consAddr sdk.ConsAddress) { + // increment time by half of consumer unbonding period + // for the undelegation to not reach maturity yet + incrementTime(suite, consumerUnbondingPeriod/2) + + // slash + suite.consumerApp.GetConsumerKeeper().Slash( + suite.consumerCtx(), + consAddr, + int64(undelegateConsumerHeight)+1, + powerAfterUndelegate, + sdk.ZeroDec(), + stakingtypes.Downtime, + ) + + // increment time so that the unbonding period ends on the consumer + incrementTime(suite, consumerUnbondingPeriod) + + // relay all packets from consumer to provider + relayAllCommittedPackets( + suite, + suite.consumerChain, + suite.path, + ccv.ConsumerPortID, + suite.path.EndpointA.ChannelID, + 3, + ) + }, + // undelegation occurred before infraction, thus it is not slashed + false, + }, + // delegate - infraction - undelegate - mature consumer - slash - mature provider + // slash + { + "infraction before undelegate, detected after maturity on consumer, before maturity on provider", + func(consAddr sdk.ConsAddress) { + // increment time so that the unbonding period ends on the consumer + incrementTime(suite, consumerUnbondingPeriod+time.Hour) + + // relay all packets from consumer to provider + relayAllCommittedPackets( + suite, + suite.consumerChain, + suite.path, + ccv.ConsumerPortID, + suite.path.EndpointA.ChannelID, + 2, + ) + + // slash + suite.consumerApp.GetConsumerKeeper().Slash( + suite.consumerCtx(), + consAddr, + int64(undelegateConsumerHeight)-1, + powerBeforeUndelegate, + sdk.ZeroDec(), + stakingtypes.Downtime, + ) + + // commit state on consumer + suite.coordinator.CommitBlock(suite.consumerChain) + + // relay all packets from consumer to provider + relayAllCommittedPackets( + suite, + suite.consumerChain, + suite.path, + ccv.ConsumerPortID, + suite.path.EndpointA.ChannelID, + 1, + ) }, - }, { - // undelegate a quarter of the new shares created - func(suite *CCVTestSuite) error { - _, err = providerStakingKeeper.Undelegate(suite.providerChain.GetContext(), delAddr, valAddr, testShares.QuoInt64(4)) - return err + // the undelegation was only matured on the consumer, + // thus the slash was successful + true, + }, + // delegate - infraction - undelegate - mature consumer - mature provider - slash + // no slash + { + "infraction before undelegate, detected after maturity on both chain", + func(consAddr sdk.ConsAddress) { + // increment time so that the unbonding period ends on the consumer + incrementTime(suite, consumerUnbondingPeriod+time.Hour) + + // relay all packets from consumer to provider + relayAllCommittedPackets( + suite, + suite.consumerChain, + suite.path, + ccv.ConsumerPortID, + suite.path.EndpointA.ChannelID, + 2, + ) + + // increment time so that the unbonding period ends on the provider + incrementTime(suite, providerUnbondingPeriod) + + // slash + suite.consumerApp.GetConsumerKeeper().Slash( + suite.consumerCtx(), + consAddr, + int64(undelegateConsumerHeight)-1, + powerBeforeUndelegate, + sdk.ZeroDec(), + stakingtypes.Downtime, + ) + + // commit state on consumer + suite.coordinator.CommitBlock(suite.consumerChain) + + // relay all packets from consumer to provider + relayAllCommittedPackets( + suite, + suite.consumerChain, + suite.path, + ccv.ConsumerPortID, + suite.path.EndpointA.ChannelID, + 1, + ) }, + // the undelegation is already matured, thus it is not slashed + false, }, } - // execute the setup operations, distributed uniformly in three blocks. - // For each of them, save their current VSC Id value which map correspond respectively - // to the block heights lesser, equal and greater than the undelegation creation height. - vscIDs := make([]uint64, 0, 3) - for _, so := range setupOperations { - err := so.fn(suite) - suite.Require().NoError(err) + for i, tc := range testCases { + providerKeeper := suite.providerApp.GetProviderKeeper() + providerStakingKeeper := suite.providerApp.GetE2eStakingKeeper() + providerSlashingKeeper := suite.providerApp.GetE2eSlashingKeeper() + + suite.SetupCCVChannel(suite.path) + + // get the power before delegate + validator, _ := suite.getValByIdx(valIndex) + powerBeforeDelegate = validator.GetConsensusPower(sdk.DefaultPowerReduction) + + // delegate some tokens + delAddr := suite.providerChain.SenderAccount.GetAddress() + initBalance, shares, valAddr := delegateByIdx(suite, delAddr, bondAmt, valIndex) + + // commit state on provider + suite.coordinator.CommitBlock(suite.providerChain) + + // relay VSCPacket w/ delegation from provider to consumer + relayAllCommittedPackets( + suite, + suite.providerChain, + suite.path, + ccv.ProviderPortID, + suite.path.EndpointB.ChannelID, + 1, + "test: "+tc.name, + ) - vscIDs = append(vscIDs, providerKeeper.GetValidatorSetUpdateId(suite.providerChain.GetContext())) - suite.providerChain.NextBlock() - } + // get the height when the consumer "applied" the delegation + delegateConsumerHeight = suite.getHeightOfVSCPacketRecv(suite.getFirstBundle(), 1, 0, "delegateConsumerHeight, test: "+tc.name) + + // get the power before undelegate + validator = suite.getVal(suite.providerCtx(), valAddr) + powerBeforeUndelegate = validator.GetConsensusPower(sdk.DefaultPowerReduction) + expectedPowerDelegated := (bondAmt.Quo(sdk.DefaultPowerReduction)).Int64() + suite.Require().Equal( + powerBeforeDelegate+expectedPowerDelegated, + powerBeforeUndelegate, + "unexpected power after delegation; test: "+tc.name, + ) - // create validator signing info to test slashing - providerSlashingKeeper.SetValidatorSigningInfo( - suite.providerChain.GetContext(), - sdk.ConsAddress(tmValidator.Address), - slashingtypes.ValidatorSigningInfo{Address: tmValidator.Address.String()}, - ) + // undelegate half of the delegated shares + vscID := undelegate(suite, delAddr, valAddr, shares.QuoInt64(2)) + // - check that staking unbonding op was created and onHold is true + checkStakingUnbondingOps(suite, 1, true, true, "test: "+tc.name) + // - check that CCV unbonding op was created + checkCCVUnbondingOp(suite, suite.providerCtx(), suite.consumerChain.ChainID, vscID, true, "test: "+tc.name) + // - check undelegation entry balance + ubd, found := providerStakingKeeper.GetUnbondingDelegation(suite.providerCtx(), delAddr, valAddr) + suite.Require().True(found) + suite.Require().True(ubd.Entries[0].Balance.Equal(bondAmt.Quo(sdk.NewInt(2)))) + + // commit state on provider + suite.coordinator.CommitBlock(suite.providerChain) + + // get the power after undelegate + validator = suite.getVal(suite.providerCtx(), valAddr) + powerAfterUndelegate = validator.GetConsensusPower(sdk.DefaultPowerReduction) + expectedPowerDelegated = (bondAmt.Quo(sdk.DefaultPowerReduction).Quo(sdk.NewInt(2))).Int64() + suite.Require().Equal( + powerBeforeDelegate+expectedPowerDelegated, + powerAfterUndelegate, + "unexpected power after undelegation; test: "+tc.name, + ) - // the test cases verify that only the unbonding tokens get slashed for the VSC ids - // mapping to the block heights before and during the undelegation otherwise not. - testCases := []struct { - expSlash bool - vscID uint64 - }{ - {expSlash: true, vscID: vscIDs[0]}, - {expSlash: true, vscID: vscIDs[1]}, - {expSlash: false, vscID: vscIDs[2]}, - } + // relay VSCPacket w/ undelegation from provider to consumer + relayAllCommittedPackets( + suite, + suite.providerChain, + suite.path, + ccv.ProviderPortID, + suite.path.EndpointB.ChannelID, + 1, + "test: "+tc.name, + ) - // save unbonding balance before slashing tests - ubd, found := providerStakingKeeper.GetUnbondingDelegation( - suite.providerChain.GetContext(), delAddr, valAddr) - suite.Require().True(found) - ubdBalance := ubd.Entries[0].Balance + // get the height when the consumer "applied" the undelegation + undelegateConsumerHeight = suite.getHeightOfVSCPacketRecv(suite.getFirstBundle(), 2, 1, "undelegateConsumerHeight; test: "+tc.name) - for _, tc := range testCases { - slashPacket := ccv.NewSlashPacketData( - abci.Validator{Address: tmValidator.Address, Power: tmValidator.VotingPower}, - tc.vscID, - stakingtypes.Downtime, + // create validator signing info to test slashing + valConsAddr, err := validator.GetConsAddr() + suite.Require().NoError(err, "test: "+tc.name) + providerSlashingKeeper.SetValidatorSigningInfo( + suite.providerChain.GetContext(), + valConsAddr, + slashingtypes.ValidatorSigningInfo{Address: valConsAddr.String()}, ) - // slash - providerKeeper.HandleSlashPacket(suite.providerChain.GetContext(), suite.consumerChain.ChainID, *slashPacket) - - ubd, found := providerStakingKeeper.GetUnbondingDelegation(suite.providerChain.GetContext(), delAddr, valAddr) + // slash validator on consumer chain + consumerKey, found := providerKeeper.GetValidatorConsumerPubKey(suite.providerCtx(), suite.consumerChain.ChainID, valConsAddr) suite.Require().True(found) + consumerAddr := utils.TMCryptoPublicKeyToConsAddr(consumerKey) + tc.slash(consumerAddr) + + // increment time so that the unbonding period ends on the provider + incrementTime(suite, providerUnbondingPeriod) + + var expectedBalance sdk.Int + var errMsg string + if tc.expSlashOccurred { + expectedBalance = initBalance.Sub(halfBondAmt).Sub(slashAmount) + errMsg = "delegator should have been slashed" + } else { + expectedBalance = initBalance.Sub(halfBondAmt) + errMsg = "delegator should not have been slashed" + } + suite.Require().Equal( + expectedBalance, + getBalance(suite, suite.providerCtx(), delAddr), + errMsg, + ) - isUbdSlashed := ubdBalance.GT(ubd.Entries[0].Balance) - suite.Require().True(tc.expSlash == isUbdSlashed) - - // update balance - ubdBalance = ubd.Entries[0].Balance + if i+1 < len(testCases) { + // reset suite to reset provider client + suite.SetupTest() + } } + } // TestValidatorDowntime tests if a slash packet is sent diff --git a/testutil/e2e/debug_test.go b/testutil/e2e/debug_test.go index 9ab98799d8..21ddf9c117 100644 --- a/testutil/e2e/debug_test.go +++ b/testutil/e2e/debug_test.go @@ -116,8 +116,8 @@ func TestOnRecvSlashPacketErrors(t *testing.T) { runCCVTestByName(t, "TestOnRecvSlashPacketErrors") } -func TestHandleSlashPacketDistribution(t *testing.T) { - runCCVTestByName(t, "TestHandleSlashPacketDistribution") +func TestSlashUndelegation(t *testing.T) { + runCCVTestByName(t, "TestSlashUndelegation") } func TestValidatorDowntime(t *testing.T) {