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!: introduce power shaping #1830

Merged
merged 20 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions proto/interchain_security/ccv/provider/v1/provider.proto
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ message ConsumerAdditionProposal {
// have to validate the proposed consumer chain. top_N can either be 0 or any value in [50, 100].
// A chain can join with top_N == 0 as an Opt In chain, or with top_N ∈ [50, 100] as a Top N chain.
uint32 top_N = 15;
// Corresponds to the maximum power (percentage-wise) a validator can have on the consumer chain. For instance, if
// `validators_power_cap` is set to 32, it means that no validator can have more than 32% of the voting power on the
// consumer chain. Note that this might not be feasible. For example, think of a consumer chain with only
// 5 validators and with `validators_power_cap` set to 10%. In such a scenario, at least one validator would need
// to have more than 20% of the total voting power. Therefore, `validators_power_cap` operates on a best-effort basis.
uint32 validators_power_cap = 16;
// Corresponds to the maximum number of validators that can validate a consumer chain.
// Only applicable to Opt In chains. Setting `validator_set_cap` on a Top N chain is a no-op.
uint32 validator_set_cap = 17;
// Corresponds to a list of provider consensus addresses of validators that are the ONLY ones that can validate
// the consumer chain.
repeated string allowlist = 18;
// Corresponds to a list of provider consensus addresses of validators that CANNOT validate the consumer chain.
repeated string denylist = 19;
}

// ConsumerRemovalProposal is a governance proposal on the provider chain to
Expand Down
4 changes: 3 additions & 1 deletion tests/mbt/driver/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/cosmos/interchain-security/v4/testutil/integration"
simibc "github.com/cosmos/interchain-security/v4/testutil/simibc"
consumertypes "github.com/cosmos/interchain-security/v4/x/ccv/consumer/types"
"github.com/cosmos/interchain-security/v4/x/ccv/provider/types"
ccvtypes "github.com/cosmos/interchain-security/v4/x/ccv/types"
)

Expand Down Expand Up @@ -377,7 +378,8 @@ func (s *Driver) ConfigureNewPath(consumerChain, providerChain *ibctesting.TestC
stakingValidators = append(stakingValidators, v)
}

nextValidators := s.providerKeeper().ComputeNextEpochConsumerValSet(s.providerCtx(), string(consumerChainId), stakingValidators, func(validator stakingtypes.Validator) bool { return true })
considerAll := func(providerAddr types.ProviderConsAddress) bool { return true }
nextValidators := s.providerKeeper().FilterValidators(s.providerCtx(), string(consumerChainId), stakingValidators, considerAll)
s.providerKeeper().SetConsumerValSet(s.providerCtx(), string(consumerChainId), nextValidators)

err = s.providerKeeper().SetConsumerGenesis(
Expand Down
8 changes: 8 additions & 0 deletions testutil/ibc_testing/generic_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
testutil "github.com/cosmos/interchain-security/v4/testutil/integration"
testkeeper "github.com/cosmos/interchain-security/v4/testutil/keeper"
consumerkeeper "github.com/cosmos/interchain-security/v4/x/ccv/consumer/keeper"
providertypes "github.com/cosmos/interchain-security/v4/x/ccv/provider/types"
)

type (
Expand Down Expand Up @@ -140,6 +141,13 @@ func AddConsumer[Tp testutil.ProviderApp, Tc testutil.ConsumerApp](
prop := testkeeper.GetTestConsumerAdditionProp()
prop.ChainId = chainID
prop.Top_N = consumerTopNParams[index] // isn't used in CreateConsumerClient

// opt-in all validators
for _, v := range providerApp.GetTestStakingKeeper().GetLastValidators(providerChain.GetContext()) {
consAddr, _ := v.GetConsAddr()
providerKeeper.SetOptedIn(providerChain.GetContext(), chainID, providertypes.NewProviderConsAddress(consAddr))
}

// NOTE: the initial height passed to CreateConsumerClient
// must be the height on the consumer when InitGenesis is called
prop.InitialHeight = clienttypes.Height{RevisionNumber: 0, RevisionHeight: 3}
Expand Down
4 changes: 4 additions & 0 deletions testutil/keeper/unit_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ func GetTestConsumerAdditionProp() *providertypes.ConsumerAdditionProposal {
types.DefaultTransferTimeoutPeriod,
types.DefaultConsumerUnbondingPeriod,
0,
0,
0,
nil,
nil,
).(*providertypes.ConsumerAdditionProposal)

return prop
Expand Down
16 changes: 13 additions & 3 deletions x/ccv/provider/client/proposal_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ Where proposal.json contains:
"ccv_timeout_period": 2419200000000000,
"unbonding_period": 1728000000000000,
"deposit": "10000stake",
"top_n": 0,
"top_n": 0,
"validators_power_cap": 32,
"validator_set_cap": 50,
"allowlist": [],
"denylist": ["validatorAConsensusAddress", "validatorBConsensusAddress"]
}
`,
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -87,7 +91,8 @@ Where proposal.json contains:
proposal.GenesisHash, proposal.BinaryHash, proposal.SpawnTime,
proposal.ConsumerRedistributionFraction, proposal.BlocksPerDistributionTransmission,
proposal.DistributionTransmissionChannel, proposal.HistoricalEntries,
proposal.CcvTimeoutPeriod, proposal.TransferTimeoutPeriod, proposal.UnbondingPeriod, proposal.TopN)
proposal.CcvTimeoutPeriod, proposal.TransferTimeoutPeriod, proposal.UnbondingPeriod, proposal.TopN,
proposal.ValidatorsPowerCap, proposal.ValidatorSetCap, proposal.Allowlist, proposal.Denylist)

from := clientCtx.GetFromAddress()

Expand Down Expand Up @@ -242,7 +247,12 @@ type ConsumerAdditionProposalJSON struct {
UnbondingPeriod time.Duration `json:"unbonding_period"`

Deposit string `json:"deposit"`
TopN uint32 `json:"top_N"`

TopN uint32 `json:"top_N"`
ValidatorsPowerCap uint32 `json:"validators_power_cap"`
ValidatorSetCap uint32 `json:"validator_set_cap"`
Allowlist []string `json:"allowlist"`
Denylist []string `json:"denylist"`
}

type ConsumerAdditionProposalReq struct {
Expand Down
213 changes: 196 additions & 17 deletions x/ccv/provider/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -1241,28 +1241,35 @@ func (k Keeper) HasToValidate(
provAddr types.ProviderConsAddress,
chainID string,
) (bool, error) {
// if the validator is opted in or was sent as part of the packet in the last epoch, they have to validate
if k.IsOptedIn(ctx, chainID, provAddr) || k.IsConsumerValidator(ctx, chainID, provAddr) {
// operate on a cached context, so we do not write (e.g., opt-in validators) to the state
cachedCtx, _ := ctx.CacheContext()

// if the validator was sent as part of the packet in the last epoch, it has to validate
if k.IsConsumerValidator(cachedCtx, chainID, provAddr) {
return true, nil
}
// otherwise, check whether the validator will be automatically opted in at the end of this epoch
// assuming all powers stay the same
val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, provAddr.ToSdkConsAddr())
if !found {
return false, fmt.Errorf("validator not found for address %s", provAddr)
}
power := k.stakingKeeper.GetLastValidatorPower(ctx, val.GetOperator())
topN, found := k.GetTopN(ctx, chainID)
if !found || topN == 0 {
return false, nil
}

minPowerToOptIn := k.ComputeMinPowerToOptIn(ctx, chainID, k.stakingKeeper.GetLastValidators(ctx), topN)
// if the validator was not part of the last epoch, check if the validator is going to be part of te next epoch
bondedValidators := k.stakingKeeper.GetLastValidators(cachedCtx)
if topN, found := k.GetTopN(cachedCtx, chainID); found && topN > 0 {
// in a Top-N chain, we automatically opt in all validators that belong to the top N
minPower := k.ComputeMinPowerToOptIn(cachedCtx, chainID, bondedValidators, topN)
k.OptInTopNValidators(cachedCtx, chainID, bondedValidators, minPower)
}

if power < minPowerToOptIn {
return false, nil
// if the validator is opted in and belongs to the validators of the next epoch, then if nothing changes
// the validator would have to validate in the next epoch
if k.IsOptedIn(cachedCtx, chainID, provAddr) {
nextValidators := k.ComputeNextValidators(cachedCtx, chainID, k.stakingKeeper.GetLastValidators(cachedCtx))
for _, v := range nextValidators {
consAddr := sdk.ConsAddress(v.ProviderConsAddr)
if provAddr.ToSdkConsAddr().Equals(consAddr) {
return true, nil
}
}
}
return true, nil

return false, nil
}

// SetConsumerCommissionRate sets a per-consumer chain commission rate
Expand Down Expand Up @@ -1337,3 +1344,175 @@ func (k Keeper) DeleteConsumerCommissionRate(
store := ctx.KVStore(k.storeKey)
store.Delete(types.ConsumerCommissionRateKey(chainID, providerAddr))
}

// SetValidatorsPowerCap sets the power-cap value `p` associated to chain with `chainID`
func (k Keeper) SetValidatorsPowerCap(
ctx sdk.Context,
chainID string,
p uint32,
) {
store := ctx.KVStore(k.storeKey)

buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, p)

store.Set(types.ValidatorsPowerCapKey(chainID), buf)
}

// DeleteValidatorsPowerCap removes the power-cap value associated to chain with `chainID`
func (k Keeper) DeleteValidatorsPowerCap(
ctx sdk.Context,
chainID string,
) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.ValidatorsPowerCapKey(chainID))
}

// GetValidatorsPowerCap returns `(p, true)` if chain `chainID` has power cap `p` associated with it, and (0, false) otherwise
func (k Keeper) GetValidatorsPowerCap(
ctx sdk.Context,
chainID string,
) (uint32, bool) {
store := ctx.KVStore(k.storeKey)
buf := store.Get(types.ValidatorsPowerCapKey(chainID))
if buf == nil {
return 0, false
}
return binary.BigEndian.Uint32(buf), true
}

// SetValidatorSetCap stores the validator-set cap value `c` associated to chain with `chainID`
func (k Keeper) SetValidatorSetCap(
ctx sdk.Context,
chainID string,
c uint32,
) {
store := ctx.KVStore(k.storeKey)

buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, c)

store.Set(types.ValidatorSetCapKey(chainID), buf)
}

// DeleteValidatorSetCap removes the validator-set cap value associated to chain with `chainID`
func (k Keeper) DeleteValidatorSetCap(
ctx sdk.Context,
chainID string,
) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.ValidatorSetCapKey(chainID))
}

// GetValidatorSetCap returns `(c, true)` if chain `chainID` has validator-set cap `c` associated with it, and (0, false) otherwise
func (k Keeper) GetValidatorSetCap(
ctx sdk.Context,
chainID string,
) (uint32, bool) {
store := ctx.KVStore(k.storeKey)
buf := store.Get(types.ValidatorSetCapKey(chainID))
if buf == nil {
return 0, false
}
return binary.BigEndian.Uint32(buf), true
}

// SetAllowlist allowlists validator with `providerAddr` address on chain `chainID`
func (k Keeper) SetAllowlist(
ctx sdk.Context,
chainID string,
providerAddr types.ProviderConsAddress,
) {
store := ctx.KVStore(k.storeKey)
store.Set(types.AllowlistCapKey(chainID, providerAddr), []byte{})
}

// IsAllowlisted returns `true` if validator with `providerAddr` has been allowlisted on chain `chainID`
func (k Keeper) IsAllowlisted(
ctx sdk.Context,
chainID string,
providerAddr types.ProviderConsAddress,
) bool {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.AllowlistCapKey(chainID, providerAddr))
return bz != nil
}

// DeleteAllowlist deletes all allowlisted validators
func (k Keeper) DeleteAllowlist(ctx sdk.Context, chainID string) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, types.ChainIdWithLenKey(types.AllowlistPrefix, chainID))
defer iterator.Close()

keysToDel := [][]byte{}
for ; iterator.Valid(); iterator.Next() {
keysToDel = append(keysToDel, iterator.Key())
}

for _, key := range keysToDel {
store.Delete(key)
}
}

// IsAllowlistEmpty returns `true` if no validator is allowlisted on chain `chainID`
func (k Keeper) IsAllowlistEmpty(ctx sdk.Context, chainID string) bool {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, types.ChainIdWithLenKey(types.AllowlistPrefix, chainID))
defer iterator.Close()

if iterator.Valid() {
return false
}

return true
}

// SetDenylist denylists validator with `providerAddr` address on chain `chainID`
func (k Keeper) SetDenylist(
ctx sdk.Context,
chainID string,
providerAddr types.ProviderConsAddress,
) {
store := ctx.KVStore(k.storeKey)
store.Set(types.DenylistCapKey(chainID, providerAddr), []byte{})
}

// IsDenylisted returns `true` if validator with `providerAddr` has been denylisted on chain `chainID`
func (k Keeper) IsDenylisted(
ctx sdk.Context,
chainID string,
providerAddr types.ProviderConsAddress,
) bool {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.DenylistCapKey(chainID, providerAddr))
return bz != nil
}

// DeleteDenylist deletes all denylisted validators
func (k Keeper) DeleteDenylist(ctx sdk.Context, chainID string) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, types.ChainIdWithLenKey(types.DenylistPrefix, chainID))
defer iterator.Close()

keysToDel := [][]byte{}
for ; iterator.Valid(); iterator.Next() {
keysToDel = append(keysToDel, iterator.Key())
}

for _, key := range keysToDel {
store.Delete(key)
}
}

// IsDenylistEmpty returns `true` if no validator is denylisted on chain `chainID`
func (k Keeper) IsDenylistEmpty(ctx sdk.Context, chainID string) bool {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, types.ChainIdWithLenKey(types.DenylistPrefix, chainID))
defer iterator.Close()

if iterator.Valid() {
return false
}

return true
}
Loading
Loading