Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Add a parameter that determines whether consumer chains allow inactive validators to validate them #2066

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add the `allow_inactive_vals` parameter for consumer chains to choose whether inactive validators can validate their chain ([\#2066](https://github.com/cosmos/interchain-security/pull/2066))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add the `allow_inactive_vals` parameter for consumer chains to choose whether inactive validators can validate their chain ([\#2066](https://github.com/cosmos/interchain-security/pull/2066))
5 changes: 4 additions & 1 deletion docs/docs/adrs/adr-017-allowing-inactive-validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ The following changes to the state are required:
* Store the provider consensus validator set in the provider module state under the `LastProviderConsensusValsPrefix` key. This is the last set of validators that the provider sent to the consensus engine. This is needed to compute the ValUpdates to send to the consensus engine (by diffing the current set with this last sent set).
* Increase the `MaxValidators` parameter of the staking module to the desired size of the potential validator
set of consumer chains.
* Introduce two extra per-consumer-chain parameters: `MinStake` and `MaxValidatorRank`. `MinStake` is the minimum amount of stake a validator must have to be considered for validation on the consumer chain. `MaxValidatorRank` is the maximum rank of a validator that can validate on the consumer chain. The provider module will only consider the first `MaxValidatorRank` validators that have at least `MinStake` stake as potential validators for the consumer chain.
* Introduce extra per-consumer-chain parameters:
* `MinStake`: is the minimum amount of stake a validator must have to be considered for validation on the consumer chain.
* `MaxValidatorRank`: is the maximum rank in the provider validator set that can validate on the consumer chain. For example, setting this to 1 means only the validator with the most stake can validate on the consumer chain.
* `AllowInactiveVals`: is a boolean that determines whether validators that are not part of the active set on the provider chain can validate on the consumer chain. If this is set to `true`, validators outside the active set on the provider chain can validate on the consumer chain. If this is set to `true`, validators outside the active set on the provider chain cannot validate on the consumer chain.
p-offtermatt marked this conversation as resolved.
Show resolved Hide resolved

## Risk Mitigations

Expand Down
8 changes: 6 additions & 2 deletions docs/docs/features/power-shaping.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ power. For example, consider that the top validator `V` on the provider chain ha
then if `V` is denylisted, the consumer chain would only be secured by at least 40% of the provider's power.
:::

1) **Maximum validator rank**: The consumer chain can specify a maximum position in the validator set that a validator can have on the provider chain to be able to validate the consumer chain. This can be used to ensure that only validators with relatively large amounts of stake can validate the consumer chain. For example, setting this to 20 would mean only the 20 validators with the most voting stake on the provider chain can validate the consumer chain.
4) **Maximum validator rank**: The consumer chain can specify a maximum position in the validator set that a validator can have on the provider chain to be able to validate the consumer chain. This can be used to ensure that only validators with relatively large amounts of stake can validate the consumer chain. For example, setting this to 20 would mean only the 20 validators with the most voting stake on the provider chain can validate the consumer chain.

2) **Minimum validator stake**: The consumer chain can specify a minimum amount of stake that a validator must have on the provider chain to be able to validate the consumer chain. This can be used to ensure that only validators with a certain amount of stake can validate the consumer chain. For example, setting this to 1000 would mean only validators with at least 1000 tokens staked on the provider chain can validate the consumer chain.
5) **Minimum validator stake**: The consumer chain can specify a minimum amount of stake that a validator must have on the provider chain to be able to validate the consumer chain. This can be used to ensure that only validators with a certain amount of stake can validate the consumer chain. For example, setting this to 1000 would mean only validators with at least 1000 tokens staked on the provider chain can validate the consumer chain.

6) **Allow inactive validators**: The consumer chain can specify whether provider validators that are *not* active in consensus may validate on the consumer chain or not. If this is set to `false`, only active validators on the provider chain can validate the consumer chain. If this is set to `true`, inactive validators can also validate the consumer chain. This can be useful for chains that want to have a larger validator set than the active validators on the provider chain, or for chains that want to have a more decentralized validator set. COnsumer chains that enable this feature should strongly consider setting a maximum validator rank and/or a minimum validator stake to ensure that only validators with some reputation/stake can validate the chain.
p-offtermatt marked this conversation as resolved.
Show resolved Hide resolved

All these mechanisms are set by the consumer chain in the `ConsumerAdditionProposal`. They operate *solely on the provider chain*, meaning the consumer chain simply receives the validator set after these rules have been applied and does not have any knowledge about whether they are applied.

Expand All @@ -43,6 +45,8 @@ Each of these mechanisms is *set during the consumer addition proposal* (see [On
The values can be seen by querying the list of consumer chains:
```bash
interchain-security-pd query provider list-consumer-chains


```

## Guidelines for setting power shaping parameters
Expand Down
4 changes: 4 additions & 0 deletions proto/interchain_security/ccv/provider/v1/provider.proto
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ message ConsumerAdditionProposal {
uint64 min_stake = 20;
// Corresponds to the maximal rank in the provider chain validator set that a validator can have to validate on the consumer chain.
uint32 max_rank = 21;
// Corresponds to whether inactive validators are allowed to validate the consumer chain.
bool allow_inactive_vals = 22;
}

// ConsumerRemovalProposal is a governance proposal on the provider chain to
Expand Down Expand Up @@ -164,6 +166,8 @@ message ConsumerModificationProposal {
uint64 min_stake = 9;
// Corresponds to the maximal rank in the provider chain validator set that a validator can have to validate on the consumer chain.
uint32 max_rank = 10;
// Corresponds to whether inactive validators are allowed to validate the consumer chain.
bool allow_inactive_vals = 11;
}


Expand Down
4 changes: 4 additions & 0 deletions proto/interchain_security/ccv/provider/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ message MsgConsumerAddition {
uint64 min_stake = 19;
// Corresponds to the maximal rank in the provider chain validator set that a validator can have to validate on the consumer chain.
uint32 max_rank = 20;
// Corresponds to whether inactive validators are allowed to validate the consumer chain.
bool allow_inactive_vals = 21;
}

// MsgConsumerAdditionResponse defines response type for MsgConsumerAddition messages
Expand Down Expand Up @@ -328,6 +330,8 @@ message MsgConsumerModification {
uint64 min_stake = 10;
// Corresponds to the maximal rank in the provider chain validator set that a validator can have to validate on the consumer chain.
uint32 max_rank = 11;
// Corresponds to whether inactive validators are allowed to validate the consumer chain.
bool allow_inactive_vals = 12;
}

message MsgConsumerModificationResponse {}
11 changes: 7 additions & 4 deletions tests/e2e/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ type SubmitConsumerAdditionProposalAction struct {
ValidatorSetCap uint32
Allowlist []string
Denylist []string
MaxValidatorRank uint32
MinStake uint64
AllowInactiveVals bool
}

func (tr Chain) submitConsumerAdditionProposal(
Expand Down Expand Up @@ -292,6 +295,9 @@ func (tr Chain) submitConsumerAdditionProposal(
ValidatorSetCap: action.ValidatorSetCap,
Allowlist: action.Allowlist,
Denylist: action.Denylist,
MaxValidatorRank: action.MaxValidatorRank,
MinStake: action.MinStake,
AllowInactiveVals: action.AllowInactiveVals,
}

bz, err := json.Marshal(prop)
Expand Down Expand Up @@ -334,7 +340,6 @@ func (tr Chain) submitConsumerAdditionProposal(
fmt.Println("submitConsumerAdditionProposal json:", jsonStr)
}
bz, err = cmd.CombinedOutput()

if err != nil {
log.Fatal(err, "\n", string(bz))
}
Expand Down Expand Up @@ -468,7 +473,6 @@ func (tr Chain) submitConsumerModificationProposal(
}

bz, err = cmd.CombinedOutput()

if err != nil {
log.Fatal(err, "\n", string(bz))
}
Expand Down Expand Up @@ -1005,7 +1009,6 @@ func (tr Chain) addChainToHermes(
action AddChainToRelayerAction,
verbose bool,
) {

bz, err := tr.target.ExecCommand("bash", "-c", "hermes", "version").CombinedOutput()
if err != nil {
log.Fatal(err, "\n error getting hermes version", string(bz))
Expand Down Expand Up @@ -1911,7 +1914,7 @@ func (tr Chain) registerRepresentative(
panic(fmt.Sprintf("failed writing ccv consumer file : %v", err))
}
defer file.Close()
err = os.WriteFile(file.Name(), []byte(fileContent), 0600)
err = os.WriteFile(file.Name(), []byte(fileContent), 0o600)
if err != nil {
log.Fatalf("Failed writing consumer genesis to file: %v", err)
}
Expand Down
15 changes: 8 additions & 7 deletions tests/e2e/steps_inactive_vals.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,13 +355,14 @@ func setupOptInChain() []Step {
return []Step{
{
Action: SubmitConsumerAdditionProposalAction{
Chain: ChainID("provi"),
From: ValidatorID("alice"),
Deposit: 10000001,
ConsumerChain: ChainID("consu"),
SpawnTime: 0,
InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1},
TopN: 0,
Chain: ChainID("provi"),
From: ValidatorID("alice"),
Deposit: 10000001,
ConsumerChain: ChainID("consu"),
SpawnTime: 0,
InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1},
TopN: 0,
AllowInactiveVals: true,
},
State: State{
ChainID("provi"): ChainState{
Expand Down
1 change: 1 addition & 0 deletions testutil/keeper/unit_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ func GetTestConsumerAdditionProp() *providertypes.ConsumerAdditionProposal {
nil,
0,
0,
false,
).(*providertypes.ConsumerAdditionProposal)

return prop
Expand Down
14 changes: 10 additions & 4 deletions x/ccv/provider/client/legacy_proposal_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ Where proposal.json contains:
"validators_power_cap": 32,
"validator_set_cap": 50,
"allowlist": [],
"denylist": ["validatorAConsensusAddress", "validatorBConsensusAddress"]
"denylist": ["validatorAConsensusAddress", "validatorBConsensusAddress"],
"min_stake": 100000000000,
"max_validator_rank": 180,
"allow_inactive_vals": false
}
`,
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -85,7 +88,7 @@ Where proposal.json contains:
proposal.DistributionTransmissionChannel, proposal.HistoricalEntries,
proposal.CcvTimeoutPeriod, proposal.TransferTimeoutPeriod, proposal.UnbondingPeriod, proposal.TopN,
proposal.ValidatorsPowerCap, proposal.ValidatorSetCap, proposal.Allowlist, proposal.Denylist,
proposal.MinStake, proposal.MaxValidatorRank)
proposal.MinStake, proposal.MaxValidatorRank, proposal.AllowInactiveVals)

from := clientCtx.GetFromAddress()

Expand Down Expand Up @@ -246,7 +249,10 @@ Where proposal.json contains:
"validators_power_cap": 32,
"validator_set_cap": 50,
"allowlist": [],
"denylist": ["validatorAConsensusAddress", "validatorBConsensusAddress"]
"denylist": ["validatorAConsensusAddress", "validatorBConsensusAddress"],
"min_stake": 100000000000,
"max_validator_rank": 180,
"allow_inactive_vals": false
}
`,
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -262,7 +268,7 @@ Where proposal.json contains:

content := types.NewConsumerModificationProposal(
proposal.Title, proposal.Summary, proposal.ChainId, proposal.TopN,
proposal.ValidatorsPowerCap, proposal.ValidatorSetCap, proposal.Allowlist, proposal.Denylist, proposal.MinStake, proposal.MaxValidatorRank)
proposal.ValidatorsPowerCap, proposal.ValidatorSetCap, proposal.Allowlist, proposal.Denylist, proposal.MinStake, proposal.MaxValidatorRank, proposal.AllowInactiveVals)

from := clientCtx.GetFromAddress()

Expand Down
2 changes: 2 additions & 0 deletions x/ccv/provider/client/legacy_proposals.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type ConsumerAdditionProposalJSON struct {
Denylist []string `json:"denylist"`
MinStake uint64 `json:"min_stake"`
MaxValidatorRank uint32 `json:"max_validator_rank"`
AllowInactiveVals bool `json:"allow_inactive_vals"`
}

type ConsumerAdditionProposalReq struct {
Expand Down Expand Up @@ -176,6 +177,7 @@ type ConsumerModificationProposalJSON struct {
Denylist []string `json:"denylist"`
MinStake uint64 `json:"min_stake"`
MaxValidatorRank uint32 `json:"max_validator_rank"`
AllowInactiveVals bool `json:"allow_inactive_vals"`

Deposit string `json:"deposit"`
}
Expand Down
5 changes: 5 additions & 0 deletions x/ccv/provider/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ func TestQueryConsumerChainsValidatorHasToValidate(t *testing.T) {
// set `providerAddr` as an opted-in validator on "chain3"
pk.SetOptedIn(ctx, "chain3", providerAddr)

// set max provider consensus vals to include all validators
params := pk.GetParams(ctx)
params.MaxProviderConsensusValidators = 180
pk.SetParams(ctx, params)

// `providerAddr` has to validate "chain1" because it is a consumer validator in this chain, as well as "chain3"
// because it opted in, in "chain3" and `providerAddr` belongs to the bonded validators
expectedChains := []string{"chain1", "chain3"}
Expand Down
43 changes: 43 additions & 0 deletions x/ccv/provider/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -1664,3 +1664,46 @@ func (k Keeper) DeleteMaxValidatorRank(
store := ctx.KVStore(k.storeKey)
store.Delete(types.MaxValidatorRankKey(chainID))
}

// SetAllowInactiveValidators sets whether inactive validators are allowed to validate
// a given consumer chain.
func (k Keeper) SetAllowInactiveValidators(
ctx sdk.Context,
chainID string,
allowed bool,
) {
store := ctx.KVStore(k.storeKey)

var buf []byte
if allowed {
buf = []byte{1}
} else {
buf = []byte{0}
}

store.Set(types.AllowInactiveValidatorsKey(chainID), buf)
}
p-offtermatt marked this conversation as resolved.
Show resolved Hide resolved

// GetAllowInactiveValidators returns whether inactive validators are allowed to validate
// a given consumer chain.
func (k Keeper) GetAllowInactiveValidators(
ctx sdk.Context,
chainID string,
) (bool, bool) {
store := ctx.KVStore(k.storeKey)
buf := store.Get(types.AllowInactiveValidatorsKey(chainID))
if buf == nil {
return false, false
}
return buf[0] == 1, true
}
p-offtermatt marked this conversation as resolved.
Show resolved Hide resolved
p-offtermatt marked this conversation as resolved.
Show resolved Hide resolved

// DeleteAllowInactiveValidators removes the flag of whether inactive validators are allowed to validate
// a given consumer chain.
func (k Keeper) DeleteAllowInactiveValidators(
ctx sdk.Context,
chainID string,
) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.AllowInactiveValidatorsKey(chainID))
}
p-offtermatt marked this conversation as resolved.
Show resolved Hide resolved
27 changes: 20 additions & 7 deletions x/ccv/provider/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,8 @@ func TestDenylist(t *testing.T) {
// - MaxValidatorRank
// - ValidatorSetCap
// - ValidatorPowersCap
func TestKeeperIntConsumerParams(t *testing.T) {
// - AllowInactiveValidators
func TestKeeperConsumerParams(t *testing.T) {
k, ctx, _, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t))

tests := []struct {
Expand Down Expand Up @@ -865,28 +866,40 @@ func TestKeeperIntConsumerParams(t *testing.T) {
initialValue: 10,
updatedValue: 11,
},
{
name: "Allow Inactive Validators",
settingFunc: func(ctx sdk.Context, id string, val int64) { k.SetAllowInactiveValidators(ctx, id, val == 1) },
getFunc: func(ctx sdk.Context, id string) (int64, bool) {
val, found := k.GetAllowInactiveValidators(ctx, id)
res := int64(0) // default value
if val {
res = 1
}
return int64(res), found
},
initialValue: 1,
updatedValue: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
chainID := "chainID"
initialValue := 1000
updatedValue := 2000
// Set initial value
tt.settingFunc(ctx, chainID, int64(initialValue))
tt.settingFunc(ctx, chainID, int64(tt.initialValue))

// Retrieve and check initial value
actualValue, found := tt.getFunc(ctx, chainID)
require.True(t, found)
require.EqualValues(t, initialValue, actualValue)
require.EqualValues(t, tt.initialValue, actualValue)

// Update value
tt.settingFunc(ctx, chainID, int64(updatedValue))
tt.settingFunc(ctx, chainID, int64(tt.updatedValue))

// Retrieve and check updated value
newActualValue, found := tt.getFunc(ctx, chainID)
require.True(t, found)
require.EqualValues(t, updatedValue, newActualValue)
require.EqualValues(t, tt.updatedValue, newActualValue)

// Check non-existent chain ID
_, found = tt.getFunc(ctx, "not the chainID")
Expand Down
1 change: 1 addition & 0 deletions x/ccv/provider/keeper/legacy_proposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func (k Keeper) HandleLegacyConsumerModificationProposal(ctx sdk.Context, p *typ
k.SetValidatorSetCap(ctx, p.ChainId, p.ValidatorSetCap)
k.SetMinStake(ctx, p.ChainId, p.MinStake)
k.SetMaxValidatorRank(ctx, p.ChainId, p.MaxRank)
k.SetAllowInactiveValidators(ctx, p.ChainId, p.AllowInactiveVals)

k.DeleteAllowlist(ctx, p.ChainId)
for _, address := range p.Allowlist {
Expand Down
8 changes: 8 additions & 0 deletions x/ccv/provider/keeper/legacy_proposal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func TestHandleLegacyConsumerAdditionProposal(t *testing.T) {
nil,
0,
0,
false,
).(*providertypes.ConsumerAdditionProposal),
blockTime: now,
expAppendProp: true,
Expand Down Expand Up @@ -96,6 +97,7 @@ func TestHandleLegacyConsumerAdditionProposal(t *testing.T) {
nil,
0,
0,
false,
).(*providertypes.ConsumerAdditionProposal),
blockTime: now,
expAppendProp: false,
Expand Down Expand Up @@ -288,6 +290,7 @@ func TestHandleConsumerModificationProposal(t *testing.T) {
providerKeeper.SetDenylist(ctx, chainID, providertypes.NewProviderConsAddress([]byte("denylistedAddr1")))
providerKeeper.SetMinStake(ctx, chainID, 1000)
providerKeeper.SetMaxValidatorRank(ctx, chainID, 180)
providerKeeper.SetAllowInactiveValidators(ctx, chainID, true)

expectedTopN := uint32(75)
expectedValidatorsPowerCap := uint32(67)
Expand All @@ -296,6 +299,7 @@ func TestHandleConsumerModificationProposal(t *testing.T) {
expectedDenylistedValidator := "cosmosvalcons1nx7n5uh0ztxsynn4sje6eyq2ud6rc6klc96w39"
expectedMinStake := uint64(0)
expectedMaxValidatorRank := uint32(20)
expectedAllowInactiveValidators := false
proposal := providertypes.NewConsumerModificationProposal("title", "description", chainID,
expectedTopN,
expectedValidatorsPowerCap,
Expand All @@ -304,6 +308,7 @@ func TestHandleConsumerModificationProposal(t *testing.T) {
[]string{expectedDenylistedValidator},
expectedMinStake,
expectedMaxValidatorRank,
expectedAllowInactiveValidators,
).(*providertypes.ConsumerModificationProposal)

err := providerKeeper.HandleLegacyConsumerModificationProposal(ctx, proposal)
Expand Down Expand Up @@ -331,4 +336,7 @@ func TestHandleConsumerModificationProposal(t *testing.T) {

actualMaxValidatorRank, _ := providerKeeper.GetMaxValidatorRank(ctx, chainID)
require.Equal(t, expectedMaxValidatorRank, actualMaxValidatorRank)

actualAllowInactiveValidators, _ := providerKeeper.GetAllowInactiveValidators(ctx, chainID)
require.Equal(t, expectedAllowInactiveValidators, actualAllowInactiveValidators)
}
Loading
Loading