/
soft_opt_out.go
249 lines (222 loc) · 9.6 KB
/
soft_opt_out.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
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/v4/x/ccv/consumer/keeper"
ccv "github.com/cosmos/interchain-security/v4/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.nextEpoch()
// 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.nextEpoch()
// 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)
suite.nextEpoch()
// 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()
}
}