Skip to content

Commit

Permalink
fix!: Avoid immediately jailing validators that are no longer opted-o…
Browse files Browse the repository at this point in the history
…ut (#1549)

* expand setupValidatorPowers method

* add comment to TestValidatorDowntime

* add soft opt out integration test

* improve soft opt out tests

* add OptedOut field to CrossChainValidator

* add SetValidatorSigningInfo to expected_keepers

* fix issue 1517

* add changelog entries

* Update tests/integration/soft_opt_out.go

Co-authored-by: Simon Noetzlin <[email protected]>

* Update tests/integration/common.go

Co-authored-by: Simon Noetzlin <[email protected]>

* Update tests/integration/slashing.go

Co-authored-by: bernd-m <[email protected]>

* remove unnecessary sort

* Update tests/integration/soft_opt_out.go

Co-authored-by: bernd-m <[email protected]>

* Update tests/integration/soft_opt_out.go

Co-authored-by: bernd-m <[email protected]>

---------

Co-authored-by: Simon Noetzlin <[email protected]>
Co-authored-by: bernd-m <[email protected]>
  • Loading branch information
3 people authored Jan 9, 2024
1 parent 5498e43 commit b636a31
Show file tree
Hide file tree
Showing 15 changed files with 415 additions and 48 deletions.
3 changes: 3 additions & 0 deletions .changelog/unreleased/bug-fixes/consumer/1549-soft-opt-out.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Avoid jailing validators immediately once they can no longer opt-out from
validating consumer chains.
([\#1549](https://github.com/cosmos/interchain-security/pull/1549))
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Avoid jailing validators immediately once they can no longer opt-out from
validating consumer chains.
([\#1549](https://github.com/cosmos/interchain-security/pull/1549))
1 change: 1 addition & 0 deletions proto/interchain_security/ccv/consumer/v1/consumer.proto
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ message CrossChainValidator {
(cosmos_proto.accepts_interface) = "cosmos.crypto.PubKey",
(gogoproto.moretags) = "yaml:\"consensus_pubkey\""
];
bool opted_out = 4;
}

// A record storing the state of a slash packet sent to the provider chain
Expand Down
19 changes: 12 additions & 7 deletions tests/integration/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,21 +596,26 @@ func (suite *CCVTestSuite) GetConsumerEndpointClientAndConsState(
}

// setupValidatorPowers delegates from the sender account to give all
// validators on the provider chain 1000 power.
func (s *CCVTestSuite) setupValidatorPowers() {
// validators on the provider chain the given voting powers.
func (s *CCVTestSuite) setupValidatorPowers(powers []int64) {
delAddr := s.providerChain.SenderAccount.GetAddress()
s.Require().Equal(len(powers), len(s.providerChain.Vals.Validators))
for idx := range s.providerChain.Vals.Validators {
delegateByIdx(s, delAddr, sdk.NewInt(999999999), idx)
bondAmt := sdk.NewInt(powers[idx]).Mul(sdk.DefaultPowerReduction)
bondAmt = bondAmt.Sub(sdk.NewInt(1)) // 1 token is bonded during the initial setup
delegateByIdx(s, delAddr, bondAmt, idx)
}

s.providerChain.NextBlock()

stakingKeeper := s.providerApp.GetTestStakingKeeper()
for _, val := range s.providerChain.Vals.Validators {
power := stakingKeeper.GetLastValidatorPower(s.providerCtx(), sdk.ValAddress(val.Address))
s.Require().Equal(int64(1000), power)
expectedTotalPower := int64(0)
for idx, val := range s.providerChain.Vals.Validators {
actualPower := stakingKeeper.GetLastValidatorPower(s.providerCtx(), sdk.ValAddress(val.Address))
s.Require().Equal(powers[idx], actualPower)
expectedTotalPower += powers[idx]
}
s.Require().Equal(int64(4000), stakingKeeper.GetLastTotalPower(s.providerCtx()).Int64())
s.Require().Equal(expectedTotalPower, stakingKeeper.GetLastTotalPower(s.providerCtx()).Int64())
}

// mustGetStakingValFromTmVal returns the staking validator from the current state of the staking keeper,
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/slashing.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ func (suite *CCVTestSuite) TestValidatorDowntime() {
ctx, ccv.ConsumerPortID, channelID)
suite.Require().True(ok)

// Sign 100 blocks
// Sign 100 blocks (default value for slashing.SignedBlocksWindow param).
valPower := int64(1)
height, signedBlocksWindow := int64(0), consumerSlashingKeeper.SignedBlocksWindow(ctx)
for ; height < signedBlocksWindow; height++ {
Expand Down
247 changes: 247 additions & 0 deletions tests/integration/soft_opt_out.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package integration

import (
"bytes"
"sort"

abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
consumerKeeper "github.com/cosmos/interchain-security/v3/x/ccv/consumer/keeper"
ccv "github.com/cosmos/interchain-security/v3/x/ccv/types"
)

// TestSoftOptOut tests the soft opt-out feature
// - if a validator in the top 95% doesn't sign 50 blocks on the consumer, a SlashPacket is sent to the provider
// - if a validator in the bottom 5% doesn't sign 50 blocks on the consumer, a SlashPacket is NOT sent to the provider
// - if a validator in the bottom 5% doesn't sign 49 blocks on the consumer,
// then it moves to the top 95% and doesn't sign one more block, a SlashPacket is NOT sent to the provider
func (suite *CCVTestSuite) TestSoftOptOut() {
var votes []abci.VoteInfo

testCases := []struct {
name string
downtimeFunc func(*consumerKeeper.Keeper, *slashingkeeper.Keeper, []byte, int)
targetValidator int
expJailed bool
expSlashPacket bool
}{
{
"downtime top 95%",
func(ck *consumerKeeper.Keeper, sk *slashingkeeper.Keeper, valAddr []byte, valIdx int) {
for i, voteInfo := range votes {
if bytes.Equal(voteInfo.Validator.Address, valAddr) {
votes[i].SignedLastBlock = false
}
}
blocksToDowntime := sk.SignedBlocksWindow(suite.consumerCtx()) - sk.MinSignedPerWindow(suite.consumerCtx()) + 1
slashingBeginBlocker(suite, votes, blocksToDowntime)
},
0,
true,
true,
},
{
"downtime bottom 5%",
func(ck *consumerKeeper.Keeper, sk *slashingkeeper.Keeper, valAddr []byte, valIdx int) {
for i, voteInfo := range votes {
if bytes.Equal(voteInfo.Validator.Address, valAddr) {
votes[i].SignedLastBlock = false
}
}
blocksToDowntime := sk.SignedBlocksWindow(suite.consumerCtx()) - sk.MinSignedPerWindow(suite.consumerCtx()) + 1
slashingBeginBlocker(suite, votes, blocksToDowntime)
},
3,
true,
false,
},
{
"downtime bottom 5% first and then top 95%, but not enough",
func(ck *consumerKeeper.Keeper, sk *slashingkeeper.Keeper, valAddr []byte, valIdx int) {
for i, voteInfo := range votes {
if bytes.Equal(voteInfo.Validator.Address, valAddr) {
votes[i].SignedLastBlock = false
}
}
blocksToDowntime := sk.SignedBlocksWindow(suite.consumerCtx()) - sk.MinSignedPerWindow(suite.consumerCtx())
slashingBeginBlocker(suite, votes, blocksToDowntime)

// Increase the power of this validator (to bring it in the top 95%)
delAddr := suite.providerChain.SenderAccount.GetAddress()
bondAmt := sdk.NewInt(100).Mul(sdk.DefaultPowerReduction)
delegateByIdx(suite, delAddr, bondAmt, valIdx)

suite.providerChain.NextBlock()

// Relay 1 VSC packet from provider to consumer
relayAllCommittedPackets(suite, suite.providerChain, suite.path, ccv.ProviderPortID, suite.path.EndpointB.ChannelID, 1)

// Update validator from store
val, found := ck.GetCCValidator(suite.consumerCtx(), valAddr)
suite.Require().True(found)
smallestNonOptOutPower := ck.GetSmallestNonOptOutPower(suite.consumerCtx())
suite.Require().Equal(val.Power, smallestNonOptOutPower)

// Let the validator continue not signing, but not enough to get jailed
for i, voteInfo := range votes {
if bytes.Equal(voteInfo.Validator.Address, valAddr) {
votes[i].Validator.Power = val.Power
}
}
slashingBeginBlocker(suite, votes, 10)
},
2,
false,
false,
},
{
"donwtime bottom 5% first and then top 95% until jailed",
func(ck *consumerKeeper.Keeper, sk *slashingkeeper.Keeper, valAddr []byte, valIdx int) {
for i, voteInfo := range votes {
if bytes.Equal(voteInfo.Validator.Address, valAddr) {
votes[i].SignedLastBlock = false
}
}
blocksToDowntime := sk.SignedBlocksWindow(suite.consumerCtx()) - sk.MinSignedPerWindow(suite.consumerCtx())
slashingBeginBlocker(suite, votes, blocksToDowntime)

// Increase the power of this validator (to bring it in the top 95%)
delAddr := suite.providerChain.SenderAccount.GetAddress()
bondAmt := sdk.NewInt(100).Mul(sdk.DefaultPowerReduction)
delegateByIdx(suite, delAddr, bondAmt, valIdx)

suite.providerChain.NextBlock()

// Relay 1 VSC packet from provider to consumer
relayAllCommittedPackets(suite, suite.providerChain, suite.path, ccv.ProviderPortID, suite.path.EndpointB.ChannelID, 1)

// Update validator from store
val, found := ck.GetCCValidator(suite.consumerCtx(), valAddr)
suite.Require().True(found)
smallestNonOptOutPower := ck.GetSmallestNonOptOutPower(suite.consumerCtx())
suite.Require().Equal(val.Power, smallestNonOptOutPower)

// Let the validator continue not signing until it gets jailed.
// Due to the starting height being just updated, the signed blocked window needs to pass.
for i, voteInfo := range votes {
if bytes.Equal(voteInfo.Validator.Address, valAddr) {
votes[i].Validator.Power = val.Power
}
}
slashingBeginBlocker(suite, votes, sk.SignedBlocksWindow(suite.consumerCtx())+1)
},
2,
true,
true,
},
}

for i, tc := range testCases {
// initial setup
suite.SetupCCVChannel(suite.path)

consumerKeeper := suite.consumerApp.GetConsumerKeeper()
consumerSlashingKeeper := suite.consumerApp.GetTestSlashingKeeper()

// Setup validator power s.t. the bottom 5% is non-empty
validatorPowers := []int64{1000, 500, 50, 10}
suite.setupValidatorPowers(validatorPowers)

// Relay 1 VSC packet from provider to consumer
relayAllCommittedPackets(suite, suite.providerChain, suite.path, ccv.ProviderPortID, suite.path.EndpointB.ChannelID, 1)

// Check that the third validator is the first in the top 95%
smallestNonOptOutPower := consumerKeeper.GetSmallestNonOptOutPower(suite.consumerCtx())
suite.Require().Equal(validatorPowers[1], smallestNonOptOutPower, "test: "+tc.name)

// Get the list of all CCV validators
vals := consumerKeeper.GetAllCCValidator(suite.consumerCtx())
// Note that GetAllCCValidator is iterating over a map so the result need to be sorted
sort.Slice(vals, func(i, j int) bool {
if vals[i].Power != vals[j].Power {
return vals[i].Power > vals[j].Power
}
return bytes.Compare(vals[i].Address, vals[j].Address) > 0
})

// Let everyone sign the first 100 blocks (default value for slahing.SignedBlocksWindow param).
// This populates the signingInfo of the slashing module so that
// the check for starting height passes.
votes = []abci.VoteInfo{}
for _, val := range vals {
votes = append(votes, abci.VoteInfo{
Validator: abci.Validator{Address: val.Address, Power: val.Power},
SignedLastBlock: true,
})
}
slashingBeginBlocker(suite, votes, consumerSlashingKeeper.SignedBlocksWindow(suite.consumerCtx()))

// Downtime infraction
sk := consumerSlashingKeeper.(slashingkeeper.Keeper)
tc.downtimeFunc(&consumerKeeper, &sk, vals[tc.targetValidator].Address, tc.targetValidator)

// Check the signing info for target validator
consAddr := sdk.ConsAddress(vals[tc.targetValidator].Address)
info, _ := consumerSlashingKeeper.GetValidatorSigningInfo(suite.consumerCtx(), consAddr)
if tc.expJailed {
// expect increased jail time
suite.Require().True(
info.JailedUntil.Equal(suite.consumerCtx().BlockTime().Add(consumerSlashingKeeper.DowntimeJailDuration(suite.consumerCtx()))),
"test: "+tc.name+"; did not update validator jailed until signing info",
)
// expect missed block counters reset
suite.Require().Zero(info.MissedBlocksCounter, "test: "+tc.name+"; did not reset validator missed block counter")
suite.Require().Zero(info.IndexOffset, "test: "+tc.name)
consumerSlashingKeeper.IterateValidatorMissedBlockBitArray(suite.consumerCtx(), consAddr, func(_ int64, missed bool) bool {
suite.Require().True(missed, "test: "+tc.name)
return false
})
} else {
suite.Require().True(
// expect not increased jail time
info.JailedUntil.Before(suite.consumerCtx().BlockTime()),
"test: "+tc.name+"; validator jailed until signing info was updated",
)
suite.Require().Positive(info.IndexOffset, "test: "+tc.name)
}

pendingPackets := consumerKeeper.GetPendingPackets(suite.consumerCtx())
if tc.expSlashPacket {
// Check that slash packet is queued
suite.Require().NotEmpty(pendingPackets, "test: "+tc.name+"; pending packets empty")
suite.Require().Len(pendingPackets, 1, "test: "+tc.name+"; pending packets len should be 1 is %d", len(pendingPackets))
cp := pendingPackets[0]
suite.Require().Equal(ccv.SlashPacket, cp.Type, "test: "+tc.name)
sp := cp.GetSlashPacketData()
suite.Require().Equal(stakingtypes.Infraction_INFRACTION_DOWNTIME, sp.Infraction, "test: "+tc.name)
suite.Require().Equal(vals[tc.targetValidator].Address, sp.Validator.Address, "test: "+tc.name)
} else {
suite.Require().Empty(pendingPackets, "test: "+tc.name+"; pending packets non-empty")
}

if i+1 < len(testCases) {
// reset suite
suite.SetupTest()
}
}
}

// slashingBeginBlocker is a mock for the slashing BeginBlocker.
// It applies the votes for a sequence of blocks
func slashingBeginBlocker(s *CCVTestSuite, votes []abci.VoteInfo, blocks int64) {
consumerSlashingKeeper := s.consumerApp.GetTestSlashingKeeper()
currentHeight := s.consumerCtx().BlockHeight()
for s.consumerCtx().BlockHeight() < currentHeight+blocks {
for _, voteInfo := range votes {
consumerSlashingKeeper.HandleValidatorSignature(
s.consumerCtx(),
voteInfo.Validator.Address,
voteInfo.Validator.Power,
voteInfo.SignedLastBlock,
)
}
s.consumerChain.NextBlock()
}
}
12 changes: 6 additions & 6 deletions tests/integration/throttle.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (s *CCVTestSuite) TestBasicSlashPacketThrottling() {

s.SetupTest()
s.SetupAllCCVChannels()
s.setupValidatorPowers()
s.setupValidatorPowers([]int64{1000, 1000, 1000, 1000})

providerStakingKeeper := s.providerApp.GetTestStakingKeeper()

Expand Down Expand Up @@ -186,7 +186,7 @@ func (s *CCVTestSuite) TestBasicSlashPacketThrottling() {
func (s *CCVTestSuite) TestMultiConsumerSlashPacketThrottling() {
// Setup test
s.SetupAllCCVChannels()
s.setupValidatorPowers()
s.setupValidatorPowers([]int64{1000, 1000, 1000, 1000})

var (
timeoutHeight = clienttypes.Height{}
Expand Down Expand Up @@ -307,7 +307,7 @@ func (s *CCVTestSuite) TestPacketSpam() {
s.SetupAllCCVChannels()

// Setup validator powers to be 25%, 25%, 25%, 25%
s.setupValidatorPowers()
s.setupValidatorPowers([]int64{1000, 1000, 1000, 1000})

// Explicitly set params, initialize slash meter
providerKeeper := s.providerApp.GetProviderKeeper()
Expand Down Expand Up @@ -373,7 +373,7 @@ func (s *CCVTestSuite) TestDoubleSignDoesNotAffectThrottling() {
s.SetupAllCCVChannels()

// Setup validator powers to be 25%, 25%, 25%, 25%
s.setupValidatorPowers()
s.setupValidatorPowers([]int64{1000, 1000, 1000, 1000})

// Explicitly set params, initialize slash meter
providerKeeper := s.providerApp.GetProviderKeeper()
Expand Down Expand Up @@ -518,7 +518,7 @@ func (s *CCVTestSuite) TestSlashMeterAllowanceChanges() {
// At first, allowance is based on 4 vals all with 1 power, min allowance is in effect.
s.Require().Equal(int64(1), providerKeeper.GetSlashMeterAllowance(s.providerCtx()).Int64())

s.setupValidatorPowers()
s.setupValidatorPowers([]int64{1000, 1000, 1000, 1000})

// Now all 4 validators have 1000 power (4000 total power) so allowance should be:
// default replenish frac * 4000 = 200
Expand All @@ -541,7 +541,7 @@ func (s CCVTestSuite) TestSlashAllValidators() { //nolint:govet // this is a tes
s.SetupAllCCVChannels()

// Setup 4 validators with 25% of the total power each.
s.setupValidatorPowers()
s.setupValidatorPowers([]int64{1000, 1000, 1000, 1000})

providerKeeper := s.providerApp.GetProviderKeeper()

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/throttle_retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func (s *CCVTestSuite) TestSlashRetries() {
s.SetupAllCCVChannels()
s.SendEmptyVSCPacket() // Establish ccv channel
s.setupValidatorPowers()
s.setupValidatorPowers([]int64{1000, 1000, 1000, 1000})

//
// Provider setup
Expand Down
8 changes: 8 additions & 0 deletions testutil/integration/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ func TestCISBeforeCCVEstablished(t *testing.T) {
runCCVTestByName(t, "TestCISBeforeCCVEstablished")
}

//
// Soft opt out tests
//

func TestSoftOptOut(t *testing.T) {
runCCVTestByName(t, "TestSoftOptOut")
}

//
// Stop consumer tests
//
Expand Down
Loading

0 comments on commit b636a31

Please sign in to comment.