Skip to content

Commit 331c1c9

Browse files
authored
feat!: only allow chains with at least one active validator to launch (#2399)
* init commit * added interchain test * added CHANGELOG entry
1 parent deea6fb commit 331c1c9

File tree

6 files changed

+161
-12
lines changed

6 files changed

+161
-12
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- `[x/provider]` Prevent Opt-In chains from launching, unless at least one active validator has opted-in to them.
2+
([\#2101](https://github.com/cosmos/interchain-security/pull/2399))

tests/interchain/chainsuite/chain_spec_provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,6 @@ func providerModifiedGenesis() []cosmos.GenesisKV {
5151
cosmos.NewGenesisKV("app_state.provider.params.slash_meter_replenish_period", ProviderReplenishPeriod),
5252
cosmos.NewGenesisKV("app_state.provider.params.slash_meter_replenish_fraction", ProviderReplenishFraction),
5353
cosmos.NewGenesisKV("app_state.provider.params.blocks_per_epoch", "1"),
54+
cosmos.NewGenesisKV("app_state.staking.params.max_validators", "1"),
5455
}
5556
}

tests/interchain/chainsuite/config.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ const (
3232
CommitTimeout = 2 * time.Second
3333
TotalValidatorFunds = 11_000_000_000
3434
ValidatorFunds = 30_000_000
35-
ValidatorCount = 1
36-
FullNodeCount = 0
37-
ChainSpawnWait = 155 * time.Second
38-
CosmosChainType = "cosmos"
39-
GovModuleAddress = "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn"
40-
TestWalletsNumber = 15 // Ensure that test accounts are used in a way that maintains the mutual independence of tests
35+
// ValidatorCount is set to 2, so we have one active and one inactive (i.e., outside the active set) validator.
36+
// Note that the provider has at most 1 validator (see `chain_spec_provider.go`).
37+
ValidatorCount = 2
38+
FullNodeCount = 0
39+
ChainSpawnWait = 155 * time.Second
40+
CosmosChainType = "cosmos"
41+
GovModuleAddress = "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn"
42+
TestWalletsNumber = 15 // Ensure that test accounts are used in a way that maintains the mutual independence of tests
4143
)
4244

4345
func DefaultConfigToml() testutil.Toml {
@@ -51,13 +53,14 @@ func DefaultConfigToml() testutil.Toml {
5153
}
5254

5355
func DefaultGenesisAmounts(denom string) func(i int) (sdktypes.Coin, sdktypes.Coin) {
56+
// Returns an amount of funds per validator, so validator with val index 0 has the most funds, then validator 1, then validator 2, etc.
5457
return func(i int) (sdktypes.Coin, sdktypes.Coin) {
5558
return sdktypes.Coin{
5659
Denom: denom,
57-
Amount: sdkmath.NewInt(TotalValidatorFunds),
60+
Amount: sdkmath.NewInt(TotalValidatorFunds / int64(i+1)),
5861
}, sdktypes.Coin{
5962
Denom: denom,
60-
Amount: sdkmath.NewInt(ValidatorFunds),
63+
Amount: sdkmath.NewInt(ValidatorFunds / int64(i+1)),
6164
}
6265
}
6366
}

tests/interchain/provider_test.go

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (s *ProviderSuite) TestProviderCreateConsumer() {
3333
testAcc := s.Provider.TestWallets[0].FormattedAddress()
3434
testAccKey := s.Provider.TestWallets[0].KeyName()
3535

36-
// Confirm that a chain can be create with the minimum params (metadata)
36+
// Confirm that a chain can be created with the minimum params (metadata)
3737
chainName := "minParamAddConsumer"
3838
createConsumerMsg := msgCreateConsumer(chainName, nil, nil, testAcc)
3939
consumerId, err := s.Provider.CreateConsumer(s.GetContext(), createConsumerMsg, testAccKey)
@@ -101,6 +101,52 @@ func (s *ProviderSuite) TestProviderCreateConsumerRejection() {
101101
s.Require().Error(err)
102102
}
103103

104+
// TestOptInChainCanOnlyStartIfActiveValidatorOptedIn tests that only if an active validator opts in to an Opt-In chain, the chain can launch.
105+
// Scenario 1: Inactive validators opts in, the chain does not launch.
106+
// Scenario 2: Active validator opts in, the chain launches.
107+
func (s *ProviderSuite) TestOptInChainCanOnlyStartIfActiveValidatorOptedIn() {
108+
testAcc := s.Provider.TestWallets[2].FormattedAddress()
109+
testAccKey := s.Provider.TestWallets[2].KeyName()
110+
111+
activeValIndex := 0
112+
inactiveValIndex := 1
113+
114+
// Scenario 1: Inactive validators opts in, the chain does not launch.
115+
chainName := "optInScenario1"
116+
spawnTime := time.Now().Add(time.Hour)
117+
consumerInitParams := consumerInitParamsTemplate(&spawnTime)
118+
createConsumerMsg := msgCreateConsumer(chainName, consumerInitParams, powerShapingParamsTemplate(), testAcc)
119+
consumerId, err := s.Provider.CreateConsumer(s.GetContext(), createConsumerMsg, testAccKey)
120+
s.Require().NoError(err)
121+
consumerChain, err := s.Provider.GetConsumerChain(s.GetContext(), consumerId)
122+
s.Require().NoError(err)
123+
// inactive validator opts in
124+
s.Require().NoError(s.Provider.OptIn(s.GetContext(), consumerChain.ConsumerID, inactiveValIndex))
125+
consumerInitParams.SpawnTime = time.Now()
126+
upgradeMsg := &providertypes.MsgUpdateConsumer{
127+
Owner: testAcc,
128+
ConsumerId: consumerChain.ConsumerID,
129+
NewOwnerAddress: testAcc,
130+
InitializationParameters: consumerInitParams,
131+
PowerShapingParameters: powerShapingParamsTemplate(),
132+
}
133+
s.Require().NoError(s.Provider.UpdateConsumer(s.GetContext(), upgradeMsg, testAccKey))
134+
s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 1, s.Provider))
135+
consumerChain, err = s.Provider.GetConsumerChain(s.GetContext(), consumerId)
136+
s.Require().NoError(err)
137+
s.Require().Equal(providertypes.CONSUMER_PHASE_REGISTERED.String(), consumerChain.Phase)
138+
139+
// Scenario 2: Active validator opts in, the chain launches.
140+
// active validator opts in
141+
s.Require().NoError(s.Provider.OptIn(s.GetContext(), consumerChain.ConsumerID, activeValIndex))
142+
143+
s.Require().NoError(s.Provider.UpdateConsumer(s.GetContext(), upgradeMsg, testAccKey))
144+
s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 1, s.Provider))
145+
consumerChain, err = s.Provider.GetConsumerChain(s.GetContext(), consumerId)
146+
s.Require().NoError(err)
147+
s.Require().Equal(providertypes.CONSUMER_PHASE_LAUNCHED.String(), consumerChain.Phase)
148+
}
149+
104150
// Test Opting in validators to a chain (MsgOptIn)
105151
// Confirm that a chain can be created and validators can be opted in
106152
// Scenario 1: Validators opted in, MsgUpdateConsumer called to set spawn time in the past -> chain should start.
@@ -403,12 +449,12 @@ func (s *ProviderSuite) TestProviderTransformTopNtoOptIn() {
403449
s.Require().Equal(testAcc, optInChain.OwnerAddress)
404450
}
405451

406-
// TestOptOut tests removin validator from consumer-opted-in-validators
452+
// TestOptOut tests removing validator from consumer-opted-in-validators
407453
func (s *ProviderSuite) TestOptOut() {
408454
testAcc := s.Provider.TestWallets[7].FormattedAddress()
409455
testAccKey := s.Provider.TestWallets[7].KeyName()
410456

411-
// Add consume chain
457+
// Add consumer chain
412458
chainName := "TestOptOut"
413459
spawnTime := time.Now().Add(time.Hour)
414460
consumerInitParams := consumerInitParamsTemplate(&spawnTime)

x/ccv/provider/keeper/consumer_lifecycle.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,33 @@ func (k Keeper) ConsumeIdsFromTimeQueue(
190190
return result, nil
191191
}
192192

193+
// HasActiveConsumerValidator checks whether at least one active validator is opted in to chain with `consumerId`
194+
func (k Keeper) HasActiveConsumerValidator(ctx sdk.Context, consumerId string, activeValidators []stakingtypes.Validator) (bool, error) {
195+
currentValidatorSet, err := k.GetConsumerValSet(ctx, consumerId)
196+
if err != nil {
197+
return false, fmt.Errorf("getting consumer validator set of chain with consumerId (%s): %w", consumerId, err)
198+
}
199+
200+
isActiveValidator := make(map[string]bool)
201+
for _, val := range activeValidators {
202+
consAddr, err := val.GetConsAddr()
203+
if err != nil {
204+
return false, fmt.Errorf("getting consensus address of validator (%+v), consumerId (%s): %w", val, consumerId, err)
205+
}
206+
providerConsAddr := types.NewProviderConsAddress(consAddr)
207+
isActiveValidator[providerConsAddr.String()] = true
208+
}
209+
210+
for _, val := range currentValidatorSet {
211+
providerConsAddr := types.NewProviderConsAddress(val.ProviderConsAddr)
212+
if isActiveValidator[providerConsAddr.String()] {
213+
return true, nil
214+
}
215+
}
216+
217+
return false, nil
218+
}
219+
193220
// LaunchConsumer launches the chain with the provided consumer id by creating the consumer client and the respective
194221
// consumer genesis file
195222
//
@@ -205,8 +232,17 @@ func (k Keeper) LaunchConsumer(
205232
if err != nil {
206233
return fmt.Errorf("computing consumer next validator set, consumerId(%s): %w", consumerId, err)
207234
}
235+
208236
if len(initialValUpdates) == 0 {
209-
return fmt.Errorf("cannot launch consumer with no validator opted in, consumerId(%s)", consumerId)
237+
return fmt.Errorf("cannot launch consumer with no consumer validator, consumerId(%s)", consumerId)
238+
}
239+
240+
hasActiveConsumerValidator, err := k.HasActiveConsumerValidator(ctx, consumerId, activeValidators)
241+
if err != nil {
242+
return fmt.Errorf("cannot check if chain has an active consumer validator, consumerId(%s): %w", consumerId, err)
243+
}
244+
if !hasActiveConsumerValidator {
245+
return fmt.Errorf("cannot launch consumer with no active consumer validator, consumerId(%s)", consumerId)
210246
}
211247

212248
// create consumer genesis

x/ccv/provider/keeper/consumer_lifecycle_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,67 @@ func TestConsumeIdsFromTimeQueue(t *testing.T) {
472472
}
473473
}
474474

475+
func TestHasActiveValidatorOptedIn(t *testing.T) {
476+
keeperParams := testkeeper.NewInMemKeeperParams(t)
477+
providerKeeper, ctx, _, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams)
478+
479+
// set 5 bonded validators with powers 5, 4, 3, 2, and 1
480+
NumberOfBondedValidators := 5
481+
var bondedValidators []stakingtypes.Validator
482+
for i := 0; i < NumberOfBondedValidators; i++ {
483+
power := int64(NumberOfBondedValidators - i)
484+
bondedValidators = append(bondedValidators, createStakingValidator(ctx, mocks, power, i))
485+
}
486+
mocks.MockStakingKeeper.EXPECT().GetBondedValidatorsByPower(gomock.Any()).Return(bondedValidators, nil).AnyTimes()
487+
488+
// get the consensus addresses of the previously-set bonded validators
489+
var consensusAddresses [][]byte
490+
for i := 0; i < NumberOfBondedValidators; i++ {
491+
consAddr, _ := bondedValidators[i].GetConsAddr()
492+
consensusAddresses = append(consensusAddresses, consAddr)
493+
}
494+
495+
// Set the maximum number of provider consensus active validators (i.e., active validators) to 3. As a result
496+
// `bondedValidators[0]` (with power of 5), `bondedValidators[1]` (with power of 4), `bondedValidators[2]` (with power of 3)
497+
// are the active validators, and `bondedValidators[3]` (with power of 2) and `bondedValidators[4]` (with power of 1)
498+
// are non-active validators.
499+
maxProviderConsensusValidators := int64(3)
500+
params := providerKeeper.GetParams(ctx)
501+
params.MaxProviderConsensusValidators = maxProviderConsensusValidators
502+
providerKeeper.SetParams(ctx, params)
503+
504+
activeValidators, _ := providerKeeper.GetLastProviderConsensusActiveValidators(ctx)
505+
506+
consumerId := "0"
507+
508+
// consumer chain has only non-active validators
509+
err := providerKeeper.SetConsumerValSet(ctx, consumerId, []providertypes.ConsensusValidator{
510+
{ProviderConsAddr: consensusAddresses[3]},
511+
{ProviderConsAddr: consensusAddresses[4]}})
512+
require.NoError(t, err)
513+
hasActiveValidatorOptedIn, err := providerKeeper.HasActiveConsumerValidator(ctx, consumerId, activeValidators)
514+
require.NoError(t, err)
515+
require.False(t, hasActiveValidatorOptedIn)
516+
517+
// consumer chain has one active validator
518+
err = providerKeeper.SetConsumerValSet(ctx, consumerId, []providertypes.ConsensusValidator{
519+
{ProviderConsAddr: consensusAddresses[2]}})
520+
require.NoError(t, err)
521+
hasActiveValidatorOptedIn, err = providerKeeper.HasActiveConsumerValidator(ctx, consumerId, activeValidators)
522+
require.NoError(t, err)
523+
require.True(t, hasActiveValidatorOptedIn)
524+
525+
// consumer chain has one active and two non-active validators
526+
err = providerKeeper.SetConsumerValSet(ctx, consumerId, []providertypes.ConsensusValidator{
527+
{ProviderConsAddr: consensusAddresses[3]},
528+
{ProviderConsAddr: consensusAddresses[4]},
529+
{ProviderConsAddr: consensusAddresses[1]}})
530+
require.NoError(t, err)
531+
hasActiveValidatorOptedIn, err = providerKeeper.HasActiveConsumerValidator(ctx, consumerId, activeValidators)
532+
require.NoError(t, err)
533+
require.True(t, hasActiveValidatorOptedIn)
534+
}
535+
475536
func TestCreateConsumerClient(t *testing.T) {
476537
type testCase struct {
477538
description string

0 commit comments

Comments
 (0)