Skip to content

Commit

Permalink
feat!: Let consumer chains choose a minimum stake and validator rank (#…
Browse files Browse the repository at this point in the history
…2035)

* Add minimum stake key

* Add MinValidatorRank prefix

* Add keeper and tests for new parameters

* Utilize MinStake and MaxRank parameters in computing next validators

* Mention MinStake and MaxRank in adr

* Add test for FulfillsMinStake

* Handle multiple validators with same power

* Add min stake and max rank to docs

* Add minStake and maxRank to proposals

* Check for untyped equality

* Handle minStake and maxRank in Msgs

* Add integration test for min stake and max rank

* Rename test and testfile

* Update docs/docs/adrs/adr-017-allowing-inactive-validators.md

* Add changelog entries for maxrank and minstake

* Address comments

* Clarify which feature is disabled by setting maxrank

* Test validator powers cap and validator set cap into int param testing function
  • Loading branch information
p-offtermatt authored Jul 17, 2024
1 parent d2ee22b commit cb44240
Show file tree
Hide file tree
Showing 27 changed files with 1,262 additions and 301 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
- Apply audit suggestions that include a bug fix in the way we compute the
maximum capped power. ([\#1925](https://github.com/cosmos/interchain-
security/pull/1925))
maximum capped power. ([\#1925](https://github.com/cosmos/interchain-security/pull/1925))
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Add minimum stake and maximum validator rank requirements to let consumer chains
determine requirements for validators that validate their chain. ([\#2035](https://github.com/cosmos/interchain-security/pull/2035))
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
- Apply audit suggestions that include a bug fix in the way we compute the
maximum capped power. ([\#1925](https://github.com/cosmos/interchain-
security/pull/1925))
maximum capped power. ([\#1925](https://github.com/cosmos/interchain-security/pull/1925))
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Add minimum stake and maximum validator rank requirements to let consumer chains
determine requirements for validators that validate their chain. ([\#2035](https://github.com/cosmos/interchain-security/pull/2035))
6 changes: 5 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,12 +62,16 @@ 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.

## Risk Mitigations

To mitigate risks from validators with little stake, we introduce a minimum stake requirement for validators to be able to validate on consumer chains, which can be set by each consumer chain independently, with a default value set by the provider chain.

Additionally, we indepdently allow individual consumer chains to disable this feature, which will disallow validators from outside the provider active set from validating on the consumer chain and revert them to the previous behaviour of only considering validators of the provider that are part of the active consensus validator set.
Additionally, we independently allow individual consumer chains to set a maximum rank for validators.
This means that validators above a certain position in the validator set cannot validate on the consumer chain.
Setting this to be equal to `MaxProviderConsensusValidators` effectively disables inactive validators from validating on the consumer chain and thus
disables the main feature described in this ADR.

Additional risk mitigations are to increase the active set size slowly, and to monitor the effects on the network closely. For the first iteration, we propose to increase the active set size to 200 validators (while keeping the consensus validators to 180), thus letting the 20 validators with the most stake outside of the active set validate on consumer chains.

Expand Down
10 changes: 7 additions & 3 deletions docs/docs/features/power-shaping.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ maximum, will validate the consumer chain.
:::info
This is only applicable to Opt In chains (chains with Top N = 0).
:::
1) **Capping the fraction of power any single validator can have**: The consumer chain can specify a maximum fraction
2) **Capping the fraction of power any single validator can have**: The consumer chain can specify a maximum fraction
of the total voting power that any single validator in its validator set should have.
This is a security measure with the intention of making it harder for a single large validator to take over a consumer chain. This mitigates the risk of an Opt In chain with only a few validators being dominated by a validator with a large amount of stake opting in.
For example, setting this fraction to e.g. 33% would mean that no single validator can have more than 33% of the total voting power,
Expand All @@ -24,17 +24,21 @@ This is a soft cap, and the actual power of a validator can exceed this fraction
Rewards are distributed proportionally to validators with respect to their capped voting power on the consumer,
not their total voting power on the provider.
:::
1) **Allowlist and denylist**: The consumer chain can specify a list of validators that are allowed or disallowed from participating in the validator set. If an allowlist is set, all validators not on the allowlist cannot validate the consumer chain. If a validator is on both lists, the denylist takes precedence, that is, they cannot validate the consumer chain. If neither list is set, all validators are able to validate the consumer chain.
3) **Allowlist and denylist**: The consumer chain can specify a list of validators that are allowed or disallowed from participating in the validator set. If an allowlist is set, all validators not on the allowlist cannot validate the consumer chain. If a validator is on both lists, the denylist takes precedence, that is, they cannot validate the consumer chain. If neither list is set, all validators are able to validate the consumer chain.

:::warning
Note that if denylisting is used in a Top N consumer chain, then the chain might not be secured by N% of the total provider's
power. For example, consider that the top validator `V` on the provider chain has 10% of the voting power, and we have a Top 50% consumer chain,
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.

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.

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.

Each of these mechanisms is *set during the consumer addition proposal* (see [Onboarding](../consumer-development/onboarding.md#3-submit-a-governance-proposal)), and is currently *immutable* after the chain has been added.
Each of these mechanisms is *set during the consumer addition proposal* (see [Onboarding](../consumer-development/onboarding.md#3-submit-a-governance-proposal)).

The values can be seen by querying the list of consumer chains:
```bash
Expand Down
8 changes: 8 additions & 0 deletions proto/interchain_security/ccv/provider/v1/provider.proto
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ message ConsumerAdditionProposal {
repeated string allowlist = 18;
// Corresponds to a list of provider consensus addresses of validators that CANNOT validate the consumer chain.
repeated string denylist = 19;
// Corresponds to the minimal amount of (provider chain) stake required to validate on the consumer chain.
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;
}

// ConsumerRemovalProposal is a governance proposal on the provider chain to
Expand Down Expand Up @@ -156,6 +160,10 @@ message ConsumerModificationProposal {
repeated string allowlist = 7;
// Corresponds to a list of provider consensus addresses of validators that CANNOT validate the consumer chain.
repeated string denylist = 8;
// Corresponds to the minimal amount of (provider chain) stake required to validate on the consumer chain.
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;
}


Expand Down
8 changes: 8 additions & 0 deletions proto/interchain_security/ccv/provider/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ message MsgConsumerAddition {
repeated string denylist = 17;
// signer address
string authority = 18 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// Corresponds to the minimal amount of (provider chain) stake required to validate on the consumer chain.
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;
}

// MsgConsumerAdditionResponse defines response type for MsgConsumerAddition messages
Expand Down Expand Up @@ -320,6 +324,10 @@ message MsgConsumerModification {
repeated string denylist = 8;
// signer address
string authority = 9 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// Corresponds to the minimal amount of (provider chain) stake required to validate on the consumer chain.
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;
}

message MsgConsumerModificationResponse {}
232 changes: 232 additions & 0 deletions tests/integration/partial_set_security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package integration

import (
"testing"

"cosmossdk.io/math"
ccv "github.com/cosmos/interchain-security/v5/x/ccv/types"
"github.com/stretchr/testify/require"

icstestingutils "github.com/cosmos/interchain-security/v5/testutil/ibc_testing"

appConsumer "github.com/cosmos/interchain-security/v5/app/consumer"
appProvider "github.com/cosmos/interchain-security/v5/app/provider"
)

// we need a stake multiplier because tokens do not directly correspond to voting power
// this is needed because 1000000 tokens = 1 voting power, so lower multipliers
// will be verbose and harder to read because small token numbers
// won't correspond to at least one voting power
const stake_multiplier = 1000000

// TestMinStakeMaxRank tests the min stake and max rank parameters.
// It starts a provider and single consumer chain,
// sets the initial powers according to the input, and then
// sets the min stake and max rank parameters according to the test case.
// Finally, it checks that the validator set on the consumer chain is as expected
// according to the min stake and max rank parameters.
func TestMinStakeMaxRank(t *testing.T) {
testCases := []struct {
name string
stakedTokens []int64
minStake uint64
maxRank uint32
expectedConsuValSet []int64
}{
{
name: "disabled min stake and max rank",
stakedTokens: []int64{
1 * stake_multiplier,
2 * stake_multiplier,
3 * stake_multiplier,
4 * stake_multiplier,
},
minStake: 0,
maxRank: 0,
expectedConsuValSet: []int64{
1 * stake_multiplier,
2 * stake_multiplier,
3 * stake_multiplier,
4 * stake_multiplier,
},
},
{
name: "stake multiplier - standard case",
stakedTokens: []int64{
1 * stake_multiplier,
2 * stake_multiplier,
3 * stake_multiplier,
4 * stake_multiplier,
},
minStake: 3 * stake_multiplier,
maxRank: 0,
expectedConsuValSet: []int64{
3 * stake_multiplier,
4 * stake_multiplier,
},
},
{
name: "validator rank - standard case",
stakedTokens: []int64{
1 * stake_multiplier,
2 * stake_multiplier,
3 * stake_multiplier,
4 * stake_multiplier,
},
minStake: 0,
maxRank: 2,
expectedConsuValSet: []int64{
3 * stake_multiplier,
4 * stake_multiplier,
},
},
{
name: "check max rank precedence over min stake",
stakedTokens: []int64{
1 * stake_multiplier,
2 * stake_multiplier,
3 * stake_multiplier,
4 * stake_multiplier,
},
minStake: 4 * stake_multiplier,
maxRank: 2,
expectedConsuValSet: []int64{
4 * stake_multiplier,
},
},
{
name: "check min stake precedence over max rank",
stakedTokens: []int64{
1 * stake_multiplier,
2 * stake_multiplier,
3 * stake_multiplier,
4 * stake_multiplier,
},
minStake: 2 * stake_multiplier,
maxRank: 1,
expectedConsuValSet: []int64{
4 * stake_multiplier,
},
},
{
name: "check min stake with multiple equal stakes",
stakedTokens: []int64{
1 * stake_multiplier,
2 * stake_multiplier,
2 * stake_multiplier,
2 * stake_multiplier,
},
minStake: 2 * stake_multiplier,
maxRank: 0,
expectedConsuValSet: []int64{
2 * stake_multiplier,
2 * stake_multiplier,
2 * stake_multiplier,
},
},
{
name: "check max rank with multiple equal stakes",
stakedTokens: []int64{
1 * stake_multiplier,
2 * stake_multiplier,
2 * stake_multiplier,
2 * stake_multiplier,
},
minStake: 0,
maxRank: 1,
expectedConsuValSet: []int64{
2 * stake_multiplier,
2 * stake_multiplier,
2 * stake_multiplier,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewCCVTestSuite[*appProvider.App, *appConsumer.App](
// Pass in ibctesting.AppIniters for provider and consumer.
icstestingutils.ProviderAppIniter, icstestingutils.ConsumerAppIniter, []string{})
s.SetT(t)
s.SetupTest()

providerKeeper := s.providerApp.GetProviderKeeper()
s.SetupCCVChannel(s.path)

// set validator powers
vals, err := providerKeeper.GetLastBondedValidators(s.providerChain.GetContext())
s.Require().NoError(err)

delegatorAccount := s.providerChain.SenderAccounts[0]

for i, val := range vals {
power := tc.stakedTokens[i]
valAddr, err := providerKeeper.ValidatorAddressCodec().StringToBytes(val.GetOperator())
s.Require().NoError(err)
undelegate(s, delegatorAccount.SenderAccount.GetAddress(), valAddr, math.LegacyOneDec())

// set validator power
delegateByIdx(s, delegatorAccount.SenderAccount.GetAddress(), math.NewInt(power), i)
}

// end the epoch to apply the updates
s.nextEpoch()

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

// end the block on the consumer to apply the updates
s.consumerChain.NextBlock()

// get the last bonded validators
lastVals, err := providerKeeper.GetLastBondedValidators(s.providerChain.GetContext())
s.Require().NoError(err)

for i, val := range lastVals {
// check that the intiial state was set correctly
require.Equal(s.T(), math.NewInt(tc.stakedTokens[i]), val.Tokens)
}

// check the validator set on the consumer chain is the original one
consuValSet := s.consumerChain.LastHeader.ValidatorSet
s.Require().Equal(len(consuValSet.Validators), 4)

// get just the powers of the consu val set
consuValPowers := make([]int64, len(consuValSet.Validators))
for i, consuVal := range consuValSet.Validators {
// voting power corresponds to staked tokens at a 1:stake_multiplier ratio
consuValPowers[i] = consuVal.VotingPower * stake_multiplier
}

s.Require().ElementsMatch(consuValPowers, tc.stakedTokens)

// adjust parameters

// set the maxRank and minStake according to the test case
providerKeeper.SetMaxValidatorRank(s.providerChain.GetContext(), s.consumerChain.ChainID, tc.maxRank)
providerKeeper.SetMinStake(s.providerChain.GetContext(), s.consumerChain.ChainID, tc.minStake)

// undelegate and delegate to trigger a vscupdate
delegateAndUndelegate(s, delegatorAccount.SenderAccount.GetAddress(), math.NewInt(1*stake_multiplier), 1)

// end the epoch to apply the updates
s.nextEpoch()

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

// end the block on the consumer to apply the updates
s.consumerChain.NextBlock()

// construct the new val powers
newConsuValSet := s.consumerChain.LastHeader.ValidatorSet
newConsuValPowers := make([]int64, len(newConsuValSet.Validators))
for i, consuVal := range newConsuValSet.Validators {
// voting power corresponds to staked tokens at a 1:stake_multiplier ratio
newConsuValPowers[i] = consuVal.VotingPower * stake_multiplier
}

// check that the new validator set is as expected
s.Require().ElementsMatch(newConsuValPowers, tc.expectedConsuValSet)
})
}
}
2 changes: 2 additions & 0 deletions testutil/keeper/unit_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ func GetTestConsumerAdditionProp() *providertypes.ConsumerAdditionProposal {
0,
nil,
nil,
0,
0,
).(*providertypes.ConsumerAdditionProposal)

return prop
Expand Down
5 changes: 3 additions & 2 deletions x/ccv/provider/client/legacy_proposal_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ Where proposal.json contains:
proposal.ConsumerRedistributionFraction, proposal.BlocksPerDistributionTransmission,
proposal.DistributionTransmissionChannel, proposal.HistoricalEntries,
proposal.CcvTimeoutPeriod, proposal.TransferTimeoutPeriod, proposal.UnbondingPeriod, proposal.TopN,
proposal.ValidatorsPowerCap, proposal.ValidatorSetCap, proposal.Allowlist, proposal.Denylist)
proposal.ValidatorsPowerCap, proposal.ValidatorSetCap, proposal.Allowlist, proposal.Denylist,
proposal.MinStake, proposal.MaxValidatorRank)

from := clientCtx.GetFromAddress()

Expand Down Expand Up @@ -261,7 +262,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.ValidatorsPowerCap, proposal.ValidatorSetCap, proposal.Allowlist, proposal.Denylist, proposal.MinStake, proposal.MaxValidatorRank)

from := clientCtx.GetFromAddress()

Expand Down
4 changes: 4 additions & 0 deletions x/ccv/provider/client/legacy_proposals.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type ConsumerAdditionProposalJSON struct {
ValidatorSetCap uint32 `json:"validator_set_cap"`
Allowlist []string `json:"allowlist"`
Denylist []string `json:"denylist"`
MinStake uint64 `json:"min_stake"`
MaxValidatorRank uint32 `json:"max_validator_rank"`
}

type ConsumerAdditionProposalReq struct {
Expand Down Expand Up @@ -172,6 +174,8 @@ type ConsumerModificationProposalJSON struct {
ValidatorSetCap uint32 `json:"validator_set_cap"`
Allowlist []string `json:"allowlist"`
Denylist []string `json:"denylist"`
MinStake uint64 `json:"min_stake"`
MaxValidatorRank uint32 `json:"max_validator_rank"`

Deposit string `json:"deposit"`
}
Expand Down
Loading

0 comments on commit cb44240

Please sign in to comment.