From 7b03c56b97f7cac4fc82a859667cf3795fd69c9d Mon Sep 17 00:00:00 2001 From: Marius Poke Date: Wed, 11 Sep 2024 11:41:35 +0200 Subject: [PATCH] feat!: remove functionality that stops validators to opt-in on multiple chains with same chain ID (#2249) * refactor: move opt-in methods to partial_set_security.go * refactor: move methods to power-shaping.go * refactor: move ComputeNextValidators to validator_set_update.go * handle error in test * remove ProviderConsAddrToOptedInConsumerIds index * make ComputeNextValidators return error * remove logger from GetLastBondedValidatorsUtil --- tests/mbt/driver/setup.go | 5 +- testutil/ibc_testing/generic_setup.go | 2 - x/ccv/consumer/keeper/keeper.go | 2 +- x/ccv/provider/keeper/consumer_lifecycle.go | 7 +- x/ccv/provider/keeper/distribution_test.go | 47 + x/ccv/provider/keeper/grpc_query.go | 10 +- x/ccv/provider/keeper/keeper.go | 63 -- x/ccv/provider/keeper/keeper_test.go | 51 -- x/ccv/provider/keeper/key_assignment_test.go | 13 +- x/ccv/provider/keeper/msg_server_test.go | 4 +- x/ccv/provider/keeper/partial_set_security.go | 357 +------- .../keeper/partial_set_security_test.go | 826 ++---------------- x/ccv/provider/keeper/permissionless.go | 112 --- x/ccv/provider/keeper/permissionless_test.go | 72 -- x/ccv/provider/keeper/power_shaping.go | 313 ++++++- x/ccv/provider/keeper/power_shaping_test.go | 691 +++++++++++++++ x/ccv/provider/keeper/relay.go | 5 +- x/ccv/provider/keeper/validator_set_update.go | 70 +- .../keeper/validator_set_update_test.go | 34 +- x/ccv/provider/migrations/v8/migrations.go | 9 - x/ccv/provider/types/keys.go | 14 +- x/ccv/provider/types/keys_test.go | 5 +- x/ccv/types/utils.go | 3 +- 23 files changed, 1260 insertions(+), 1455 deletions(-) diff --git a/tests/mbt/driver/setup.go b/tests/mbt/driver/setup.go index e157f4ce60..0db5370578 100644 --- a/tests/mbt/driver/setup.go +++ b/tests/mbt/driver/setup.go @@ -377,8 +377,9 @@ func (s *Driver) ConfigureNewPath(consumerChain, providerChain *ibctesting.TestC stakingValidators = append(stakingValidators, v) } - considerAll := func(providerAddr providertypes.ProviderConsAddress) bool { return true } - nextValidators := s.providerKeeper().FilterValidators(s.providerCtx(), string(consumerChainId), stakingValidators, considerAll) + considerAll := func(providerAddr providertypes.ProviderConsAddress) (bool, error) { return true, nil } + nextValidators, err := s.providerKeeper().FilterValidators(s.providerCtx(), string(consumerChainId), stakingValidators, considerAll) + require.NoError(s.t, err) err = s.providerKeeper().SetConsumerValSet(s.providerCtx(), string(consumerChainId), nextValidators) require.NoError(s.t, err) diff --git a/testutil/ibc_testing/generic_setup.go b/testutil/ibc_testing/generic_setup.go index c0f1bfd7ba..680dc842b5 100644 --- a/testutil/ibc_testing/generic_setup.go +++ b/testutil/ibc_testing/generic_setup.go @@ -176,8 +176,6 @@ func AddConsumer[Tp testutil.ProviderApp, Tc testutil.ConsumerApp]( for _, v := range lastVals { consAddr, _ := v.GetConsAddr() providerKeeper.SetOptedIn(providerChain.GetContext(), consumerId, providertypes.NewProviderConsAddress(consAddr)) - err = providerKeeper.AppendOptedInConsumerId(providerChain.GetContext(), providertypes.NewProviderConsAddress(consAddr), consumerId) - s.Require().NoError(err) } // commit the state on the provider chain diff --git a/x/ccv/consumer/keeper/keeper.go b/x/ccv/consumer/keeper/keeper.go index 57896cb973..67146f7b8c 100644 --- a/x/ccv/consumer/keeper/keeper.go +++ b/x/ccv/consumer/keeper/keeper.go @@ -720,5 +720,5 @@ func (k Keeper) GetLastBondedValidators(ctx sdk.Context) ([]stakingtypes.Validat if err != nil { return nil, err } - return ccv.GetLastBondedValidatorsUtil(ctx, k.standaloneStakingKeeper, k.Logger(ctx), maxVals) + return ccv.GetLastBondedValidatorsUtil(ctx, k.standaloneStakingKeeper, maxVals) } diff --git a/x/ccv/provider/keeper/consumer_lifecycle.go b/x/ccv/provider/keeper/consumer_lifecycle.go index adda4902d1..a93d98efa0 100644 --- a/x/ccv/provider/keeper/consumer_lifecycle.go +++ b/x/ccv/provider/keeper/consumer_lifecycle.go @@ -335,10 +335,13 @@ func (k Keeper) MakeConsumerGenesis( } // need to use the bondedValidators, not activeValidators, here since the chain might be opt-in and allow inactive vals - nextValidators := k.ComputeNextValidators(ctx, consumerId, bondedValidators, powerShapingParameters, minPower) + nextValidators, err := k.ComputeNextValidators(ctx, consumerId, bondedValidators, powerShapingParameters, minPower) + if err != nil { + return gen, nil, fmt.Errorf("unable to compute the next validators in MakeConsumerGenesis, consumerId(%s): %w", consumerId, err) + } err = k.SetConsumerValSet(ctx, consumerId, nextValidators) if err != nil { - return gen, nil, fmt.Errorf("unable to set consumer validator set in MakeConsumerGenesis: %s", err) + return gen, nil, fmt.Errorf("unable to set consumer validator set in MakeConsumerGenesis, consumerId(%s): %w", consumerId, err) } // get the initial updates with the latest set consumer public keys diff --git a/x/ccv/provider/keeper/distribution_test.go b/x/ccv/provider/keeper/distribution_test.go index 1fa48eb49f..ba48758c21 100644 --- a/x/ccv/provider/keeper/distribution_test.go +++ b/x/ccv/provider/keeper/distribution_test.go @@ -330,3 +330,50 @@ func TestChangeRewardDenoms(t *testing.T) { attributes = keeper.ChangeRewardDenoms(ctx, denomsToAdd, denomsToRemove) require.Len(t, attributes, 0) // No attributes should be returned since the denom is not registered } + +func TestHandleSetConsumerCommissionRate(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + providerAddr := providertypes.NewProviderConsAddress([]byte("providerAddr")) + + // trying to set a commission rate to a unknown consumer chain + require.Error(t, providerKeeper.HandleSetConsumerCommissionRate(ctx, "unknownChainID", providerAddr, math.LegacyZeroDec())) + + // setup a pending consumer chain + consumerId := "0" + providerKeeper.FetchAndIncrementConsumerId(ctx) + providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_INITIALIZED) + + // check that there's no commission rate set for the validator yet + _, found := providerKeeper.GetConsumerCommissionRate(ctx, consumerId, providerAddr) + require.False(t, found) + + mocks.MockStakingKeeper.EXPECT().MinCommissionRate(ctx).Return(math.LegacyZeroDec(), nil).Times(1) + require.NoError(t, providerKeeper.HandleSetConsumerCommissionRate(ctx, consumerId, providerAddr, math.LegacyOneDec())) + + // check that the commission rate is now set + cr, found := providerKeeper.GetConsumerCommissionRate(ctx, consumerId, providerAddr) + require.Equal(t, math.LegacyOneDec(), cr) + require.True(t, found) + + // set minimum rate of 1/2 + commissionRate := math.LegacyNewDec(1).Quo(math.LegacyNewDec(2)) + mocks.MockStakingKeeper.EXPECT().MinCommissionRate(ctx).Return(commissionRate, nil).AnyTimes() + + // try to set a rate slightly below the minimum + require.Error(t, providerKeeper.HandleSetConsumerCommissionRate( + ctx, + consumerId, + providerAddr, + commissionRate.Sub(math.LegacyNewDec(1).Quo(math.LegacyNewDec(100)))), // 0.5 - 0.01 + "commission rate should be rejected (below min), but is not", + ) + + // set a valid commission equal to the minimum + require.NoError(t, providerKeeper.HandleSetConsumerCommissionRate(ctx, consumerId, providerAddr, commissionRate)) + // check that the rate was set + cr, found = providerKeeper.GetConsumerCommissionRate(ctx, consumerId, providerAddr) + require.Equal(t, commissionRate, cr) + require.True(t, found) +} diff --git a/x/ccv/provider/keeper/grpc_query.go b/x/ccv/provider/keeper/grpc_query.go index cebc952d60..c18f44f548 100644 --- a/x/ccv/provider/keeper/grpc_query.go +++ b/x/ccv/provider/keeper/grpc_query.go @@ -348,7 +348,10 @@ func (k Keeper) QueryConsumerValidators(goCtx context.Context, req *types.QueryC } } - consumerValSet = k.ComputeNextValidators(ctx, consumerId, bondedValidators, powerShapingParameters, minPower) + consumerValSet, err = k.ComputeNextValidators(ctx, consumerId, bondedValidators, powerShapingParameters, minPower) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to compute the next validators for chain %s: %s", consumerId, err)) + } // sort the address of the validators by ascending lexical order as they were persisted to the store sort.Slice(consumerValSet, func(i, j int) bool { @@ -476,7 +479,10 @@ func (k Keeper) hasToValidate( if err != nil { return false, err } - nextValidators := k.ComputeNextValidators(ctx, consumerId, lastVals, powerShapingParameters, minPowerToOptIn) + nextValidators, err := k.ComputeNextValidators(ctx, consumerId, lastVals, powerShapingParameters, minPowerToOptIn) + if err != nil { + return false, err + } for _, v := range nextValidators { consAddr := sdk.ConsAddress(v.ProviderConsAddr) if provAddr.ToSdkConsAddr().Equals(consAddr) { diff --git a/x/ccv/provider/keeper/keeper.go b/x/ccv/provider/keeper/keeper.go index ea2f3fc229..55ddcf413f 100644 --- a/x/ccv/provider/keeper/keeper.go +++ b/x/ccv/provider/keeper/keeper.go @@ -735,69 +735,6 @@ func (k Keeper) GetAllActiveConsumerIds(ctx sdk.Context) []string { return consumerIds } -func (k Keeper) SetOptedIn( - ctx sdk.Context, - consumerId string, - providerConsAddress types.ProviderConsAddress, -) { - store := ctx.KVStore(k.storeKey) - store.Set(types.OptedInKey(consumerId, providerConsAddress), []byte{}) -} - -func (k Keeper) DeleteOptedIn( - ctx sdk.Context, - consumerId string, - providerAddr types.ProviderConsAddress, -) { - store := ctx.KVStore(k.storeKey) - store.Delete(types.OptedInKey(consumerId, providerAddr)) -} - -func (k Keeper) IsOptedIn( - ctx sdk.Context, - consumerId string, - providerAddr types.ProviderConsAddress, -) bool { - store := ctx.KVStore(k.storeKey) - return store.Get(types.OptedInKey(consumerId, providerAddr)) != nil -} - -// GetAllOptedIn returns all the opted-in validators on chain `consumerId` -func (k Keeper) GetAllOptedIn( - ctx sdk.Context, - consumerId string, -) (providerConsAddresses []types.ProviderConsAddress) { - store := ctx.KVStore(k.storeKey) - key := types.StringIdWithLenKey(types.OptedInKeyPrefix(), consumerId) - iterator := storetypes.KVStorePrefixIterator(store, key) - defer iterator.Close() - - for ; iterator.Valid(); iterator.Next() { - providerConsAddresses = append(providerConsAddresses, types.NewProviderConsAddress(iterator.Key()[len(key):])) - } - - return providerConsAddresses -} - -// DeleteAllOptedIn deletes all the opted-in validators for chain with `consumerId` -func (k Keeper) DeleteAllOptedIn( - ctx sdk.Context, - consumerId string, -) { - store := ctx.KVStore(k.storeKey) - key := types.StringIdWithLenKey(types.OptedInKeyPrefix(), consumerId) - iterator := storetypes.KVStorePrefixIterator(store, key) - - var keysToDel [][]byte - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - keysToDel = append(keysToDel, iterator.Key()) - } - for _, delKey := range keysToDel { - store.Delete(delKey) - } -} - // SetConsumerCommissionRate sets a per-consumer chain commission rate // for the given validator address func (k Keeper) SetConsumerCommissionRate( diff --git a/x/ccv/provider/keeper/keeper_test.go b/x/ccv/provider/keeper/keeper_test.go index df6e811d17..68a6381421 100644 --- a/x/ccv/provider/keeper/keeper_test.go +++ b/x/ccv/provider/keeper/keeper_test.go @@ -1,7 +1,6 @@ package keeper_test import ( - "bytes" "fmt" "sort" "testing" @@ -293,56 +292,6 @@ func TestSetSlashLog(t *testing.T) { require.False(t, providerKeeper.GetSlashLog(ctx, addrWithoutDoubleSigns)) } -func TestGetAllOptedIn(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - expectedOptedInValidators := []providertypes.ProviderConsAddress{ - providertypes.NewProviderConsAddress([]byte("providerAddr1")), - providertypes.NewProviderConsAddress([]byte("providerAddr2")), - providertypes.NewProviderConsAddress([]byte("providerAddr3")), - } - - for _, expectedOptedInValidator := range expectedOptedInValidators { - providerKeeper.SetOptedIn(ctx, CONSUMER_ID, expectedOptedInValidator) - } - - actualOptedInValidators := providerKeeper.GetAllOptedIn(ctx, CONSUMER_ID) - - // sort validators first to be able to compare - sortOptedInValidators := func(addresses []providertypes.ProviderConsAddress) { - sort.Slice(addresses, func(i, j int) bool { - return bytes.Compare(addresses[i].ToSdkConsAddr(), addresses[j].ToSdkConsAddr()) < 0 - }) - } - sortOptedInValidators(expectedOptedInValidators) - sortOptedInValidators(actualOptedInValidators) - require.Equal(t, expectedOptedInValidators, actualOptedInValidators) -} - -// TestOptedIn tests the `SetOptedIn`, `IsOptedIn`, `DeleteOptedIn` and `DeleteAllOptedIn` methods -func TestOptedIn(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - optedInValidator1 := providertypes.NewProviderConsAddress([]byte("providerAddr1")) - optedInValidator2 := providertypes.NewProviderConsAddress([]byte("providerAddr2")) - - require.False(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator1)) - providerKeeper.SetOptedIn(ctx, CONSUMER_ID, optedInValidator1) - require.True(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator1)) - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, optedInValidator1) - require.False(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator1)) - - providerKeeper.SetOptedIn(ctx, CONSUMER_ID, optedInValidator1) - providerKeeper.SetOptedIn(ctx, CONSUMER_ID, optedInValidator2) - require.True(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator1)) - require.True(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator2)) - providerKeeper.DeleteAllOptedIn(ctx, CONSUMER_ID) - require.False(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator1)) - require.False(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator2)) -} - // TestConsumerCommissionRate tests the `SetConsumerCommissionRate`, `GetConsumerCommissionRate`, and `DeleteConsumerCommissionRate` methods func TestConsumerCommissionRate(t *testing.T) { providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) diff --git a/x/ccv/provider/keeper/key_assignment_test.go b/x/ccv/provider/keeper/key_assignment_test.go index c4d07de990..904a6c795c 100644 --- a/x/ccv/provider/keeper/key_assignment_test.go +++ b/x/ccv/provider/keeper/key_assignment_test.go @@ -788,14 +788,15 @@ func TestSimulatedAssignmentsAndUpdateApplication(t *testing.T) { }) } - nextValidators := k.FilterValidators(ctx, CONSUMERID, bondedValidators, - func(providerAddr types.ProviderConsAddress) bool { - return true + nextValidators, err := k.FilterValidators(ctx, CONSUMERID, bondedValidators, + func(providerAddr types.ProviderConsAddress) (bool, error) { + return true, nil }) - valSet, error := k.GetConsumerValSet(ctx, CONSUMERID) - require.NoError(t, error) + require.NoError(t, err) + valSet, err := k.GetConsumerValSet(ctx, CONSUMERID) + require.NoError(t, err) updates = providerkeeper.DiffValidators(valSet, nextValidators) - err := k.SetConsumerValSet(ctx, CONSUMERID, nextValidators) + err = k.SetConsumerValSet(ctx, CONSUMERID, nextValidators) require.NoError(t, err) consumerValset.apply(updates) diff --git a/x/ccv/provider/keeper/msg_server_test.go b/x/ccv/provider/keeper/msg_server_test.go index e9689eb943..d90fde30ce 100644 --- a/x/ccv/provider/keeper/msg_server_test.go +++ b/x/ccv/provider/keeper/msg_server_test.go @@ -1,10 +1,11 @@ package keeper_test import ( - "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" "testing" "time" + "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + "github.com/stretchr/testify/require" "github.com/cosmos/cosmos-sdk/codec/address" @@ -242,6 +243,7 @@ func TestUpdateConsumer(t *testing.T) { InitializationParameters: &expectedInitializationParameters, PowerShapingParameters: nil, }) + require.NoError(t, err) // assert the chain is not scheduled to launch consumerIds, err = providerKeeper.GetConsumersToBeLaunched(ctx, previousSpawnTime) require.NoError(t, err) diff --git a/x/ccv/provider/keeper/partial_set_security.go b/x/ccv/provider/keeper/partial_set_security.go index ce024453d7..f96a47ceee 100644 --- a/x/ccv/provider/keeper/partial_set_security.go +++ b/x/ccv/provider/keeper/partial_set_security.go @@ -1,11 +1,8 @@ package keeper import ( - "fmt" - "sort" - errorsmod "cosmossdk.io/errors" - "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -23,25 +20,7 @@ func (k Keeper) HandleOptIn(ctx sdk.Context, consumerId string, providerAddr typ "cannot opt in to a consumer chain that is not in the registered, initialized, or launched phase: %s", consumerId) } - chainId, err := k.GetConsumerChainId(ctx, consumerId) - if err != nil { - // TODO (PERMISSIONLESS): fix error types - return errorsmod.Wrapf( - types.ErrUnknownConsumerId, - "opting in to an unknown consumer chain, with id (%s): %s", consumerId, err.Error()) - } - optedInToConsumerId, found := k.IsValidatorOptedInToChainId(ctx, providerAddr, chainId) - if found { - return errorsmod.Wrapf(types.ErrAlreadyOptedIn, - "validator is already opted in to a chain (%s) with this chain id (%s)", - optedInToConsumerId, chainId) - } - k.SetOptedIn(ctx, consumerId, providerAddr) - err = k.AppendOptedInConsumerId(ctx, providerAddr, consumerId) - if err != nil { - return err - } if consumerKey != "" { consumerTMPublicKey, err := k.ParseConsumerKey(consumerKey) @@ -117,7 +96,8 @@ func (k Keeper) HandleOptOut(ctx sdk.Context, consumerId string, providerAddr ty } k.DeleteOptedIn(ctx, consumerId, providerAddr) - return k.RemoveOptedInConsumerId(ctx, providerAddr, consumerId) + + return nil } // OptInTopNValidators opts in to `consumerId` all the `bondedValidators` that have at least `minPowerToOptIn` power @@ -152,323 +132,74 @@ func (k Keeper) OptInTopNValidators(ctx sdk.Context, consumerId string, bondedVa k.Logger(ctx).Debug("Opting in validator", "validator", val.GetOperator()) // if validator already exists it gets overwritten - err = k.AppendOptedInConsumerId(ctx, types.NewProviderConsAddress(consAddr), consumerId) - if err != nil { - k.Logger(ctx).Error("could not append validator as opted-in validator for this consumer chain", - "validator operator address", val.GetOperator(), - "consumer id", consumerId, - "error", err.Error()) - continue - } k.SetOptedIn(ctx, consumerId, types.NewProviderConsAddress(consAddr)) } // else validators that do not belong to the top N validators but were opted in, remain opted in } } -// ComputeMinPowerInTopN returns the minimum power needed for a validator (from the bonded validators) -// to belong to the `topN`% of validators for a Top N chain. -func (k Keeper) ComputeMinPowerInTopN(ctx sdk.Context, bondedValidators []stakingtypes.Validator, topN uint32) (int64, error) { - if topN == 0 || topN > 100 { - // Note that Top N chains have a lower limit on `topN`, namely that topN cannot be less than 50. - // However, we can envision that this method could be used for other (future) reasons where this might not - // be the case. For this, this method operates for `topN`s in (0, 100]. - return 0, fmt.Errorf("trying to compute minimum power with an incorrect"+ - " topN value (%d). topN has to be in (0, 100]", topN) - } - - totalPower := math.LegacyZeroDec() - var powers []int64 +// +// Setters and getters +// - for _, val := range bondedValidators { - valAddr, err := sdk.ValAddressFromBech32(val.GetOperator()) - if err != nil { - return 0, err - } - power, err := k.stakingKeeper.GetLastValidatorPower(ctx, valAddr) - if err != nil { - return 0, err - } - powers = append(powers, power) - totalPower = totalPower.Add(math.LegacyNewDec(power)) - } - - // sort by powers descending - sort.Slice(powers, func(i, j int) bool { - return powers[i] > powers[j] - }) - - topNThreshold := math.LegacyNewDec(int64(topN)).QuoInt64(int64(100)) - powerSum := math.LegacyZeroDec() - for _, power := range powers { - powerSum = powerSum.Add(math.LegacyNewDec(power)) - if powerSum.Quo(totalPower).GTE(topNThreshold) { - return power, nil - } - } - - // We should never reach this point because the topN can be up to 1.0 (100%) and in the above `for` loop we - // perform an equal comparison as well (`GTE`). - return 0, fmt.Errorf("should never reach this point with topN (%d), totalPower (%d), and powerSum (%d)", topN, totalPower, powerSum) -} - -// CapValidatorSet caps the provided `validators` if chain with `consumerId` is an Opt In chain with a validator-set cap. -// If cap is `k`, `CapValidatorSet` returns the first `k` validators from `validators` with the highest power. -func (k Keeper) CapValidatorSet( - ctx sdk.Context, - powerShapingParameters types.PowerShapingParameters, - validators []types.ConsensusValidator, -) []types.ConsensusValidator { - if powerShapingParameters.Top_N > 0 { - // is a no-op if the chain is a Top N chain - return validators - } - - validatorSetCap := powerShapingParameters.ValidatorSetCap - if validatorSetCap != 0 && int(validatorSetCap) < len(validators) { - sort.Slice(validators, func(i, j int) bool { - return validators[i].Power > validators[j].Power - }) - - return validators[:int(validatorSetCap)] - } else { - return validators - } -} - -// CapValidatorsPower caps the power of the validators on chain with `consumerId` and returns an updated slice of validators -// with their new powers. Works on a best-basis effort because there are cases where we cannot guarantee that all validators -// on the consumer chain have less power than the set validators-power cap. For example, if we have 10 validators and -// the power cap is set to 5%, we need at least one validator to have more than 10% of the voting power on the consumer chain. -func (k Keeper) CapValidatorsPower( +func (k Keeper) SetOptedIn( ctx sdk.Context, - validatorsPowerCap uint32, - validators []types.ConsensusValidator, -) []types.ConsensusValidator { - if validatorsPowerCap > 0 { - return NoMoreThanPercentOfTheSum(validators, validatorsPowerCap) - } else { - // is a no-op if power cap is not set for `consumerId` - return validators - } -} - -// sum is a helper function to sum all the validators' power -func sum(validators []types.ConsensusValidator) int64 { - s := int64(0) - for _, v := range validators { - s += v.Power - } - return s -} - -// NoMoreThanPercentOfTheSum returns a set of validators with updated powers such that no validator has more than the -// provided `percent` of the sum of all validators' powers. Operates on a best-effort basis. -func NoMoreThanPercentOfTheSum(validators []types.ConsensusValidator, percent uint32) []types.ConsensusValidator { - // Algorithm's idea - // ---------------- - // Consider the validators' powers to be `a_1, a_2, ... a_n` and `p` to be the percent in [1, 100]. Now, consider - // the sum `s = a_1 + a_2 + ... + a_n`. Then `maxPower = s * p / 100 <=> 100 * maxPower = s * p`. - // The problem of capping the validators' powers to be no more than `p` has no solution if `(100 / n) > p`. For example, - // for n = 10 and p = 5 we do not have a solution. We would need at least one validator with power greater than 10% - // for a solution to exist. - // So, if `(100 / n) > p` there's no solution. We know that `100 * maxPower = s * p` and so `(100 / n) > (100 * maxPower) / s` - // `100 * s > 100 * maxPower * n <=> s > maxPower * n`. Thus, we do not have a solution if `s > n * maxPower`. - - // If `s <= n * maxPower` the idea of the algorithm is rather simple. - // - Compute the `maxPower` a validator must have so that it does not have more than `percent` of the voting power. - // - If a validator `v` has power `p`, then: - // - if `p > maxPower` we set `v`'s power to `maxPower` and distribute the `p - maxPower` to validators that - // have less than `maxPower` power. This way, the total sum remains the same and no validator has more than - // `maxPower` and so the power capping is satisfied. - // - Note that in order to avoid setting multiple validators to `maxPower`, we first compute all the `remainingPower` - // we have to distribute and then attempt to add `remainingPower / validatorsWithPowerLessThanMaxPower` to each validator. - // To guarantee that the sum remains the same after the distribution of powers, we sort the powers in descending - // order. This way, we first attempt to add `remainingPower / validatorsWithPowerLessThanMaxPower` to validators - // with greater power and if we cannot add the `remainingPower / validatorsWithPowerLessThanMaxPower` without - // exceeding `maxPower`, we just add enough to reach `maxPower` and add the remaining power to validators with smaller - // power. - - // If `s > n * maxPower` there's no solution and the algorithm would set everything to `maxPower`. - // ---------------- - - // Computes `floor((sum(validators) * percent) / 100)` - maxPower := math.LegacyNewDec(sum(validators)).Mul(math.LegacyNewDec(int64(percent))).QuoInt64(100).TruncateInt64() - - if maxPower == 0 { - // edge case: set `maxPower` to 1 to avoid setting the power of a validator to 0 - maxPower = 1 - } - - // Sort by `.Power` in decreasing order. Sorting in descending order is needed because otherwise we would have cases - // like this `powers =[60, 138, 559]` and `p = 35%` where sum is `757` and `maxValue = 264`. - // Because `559 - 264 = 295` we have to distribute 295 to the first 2 numbers (`295/2 = 147` to each number). However, - // note that `138 + 147 > 264`. If we were to add 147 to 60 first, then we cannot give the remaining 147 to 138. - // So, the idea is to first give `126 (= 264 - 138)` to 138, so it becomes 264, and then add `21 (=147 - 26) + 147` to 60. - sort.Slice(validators, func(i, j int) bool { - return validators[i].Power > validators[j].Power - }) - - // `remainingPower` is to be distributed to validators that have power less than `maxPower` - remainingPower := int64(0) - validatorsWithPowerLessThanMaxPower := 0 - for _, v := range validators { - if v.Power >= maxPower { - remainingPower += (v.Power - maxPower) - } else { - validatorsWithPowerLessThanMaxPower++ - } - } - - updatedValidators := make([]types.ConsensusValidator, len(validators)) - - powerPerValidator := int64(0) - remainingValidators := int64(validatorsWithPowerLessThanMaxPower) - if remainingValidators != 0 { - // power to give to each validator in order to distribute the `remainingPower` - powerPerValidator = remainingPower / remainingValidators - } - - for i, v := range validators { - if v.Power >= maxPower { - updatedValidators[i] = validators[i] - updatedValidators[i].Power = maxPower - } else if v.Power+powerPerValidator >= maxPower { - updatedValidators[i] = validators[i] - updatedValidators[i].Power = maxPower - remainingPower -= (maxPower - v.Power) - remainingValidators-- - } else { - updatedValidators[i] = validators[i] - updatedValidators[i].Power = v.Power + powerPerValidator - remainingPower -= (updatedValidators[i].Power - validators[i].Power) - remainingValidators-- - } - if remainingValidators == 0 { - continue - } - powerPerValidator = remainingPower / remainingValidators - } - - return updatedValidators + consumerId string, + providerConsAddress types.ProviderConsAddress, +) { + store := ctx.KVStore(k.storeKey) + store.Set(types.OptedInKey(consumerId, providerConsAddress), []byte{}) } -// CanValidateChain returns true if the validator `providerAddr` is opted-in to chain with `consumerId` and the allowlist -// and denylist do not prevent the validator from validating the chain. -func (k Keeper) CanValidateChain( +func (k Keeper) DeleteOptedIn( ctx sdk.Context, consumerId string, providerAddr types.ProviderConsAddress, - topN uint32, - minPowerToOptIn int64, -) bool { - // check if the validator is already opted-in - optedIn := k.IsOptedIn(ctx, consumerId, providerAddr) - - // check if the validator is automatically opted-in for a topN chain - if !optedIn && topN > 0 { - optedIn = k.HasMinPower(ctx, providerAddr, minPowerToOptIn) - } - - // only consider opted-in validators - return optedIn && - // if an allowlist is declared, only consider allowlisted validators - (k.IsAllowlistEmpty(ctx, consumerId) || - k.IsAllowlisted(ctx, consumerId, providerAddr)) && - // if a denylist is declared, only consider denylisted validators - (k.IsDenylistEmpty(ctx, consumerId) || - !k.IsDenylisted(ctx, consumerId, providerAddr)) +) { + store := ctx.KVStore(k.storeKey) + store.Delete(types.OptedInKey(consumerId, providerAddr)) } -// FulfillsMinStake returns true if the validator `providerAddr` has enough stake to validate chain with `consumerId` -// by checking its staked tokens against the minimum stake required to validate the chain. -func (k Keeper) FulfillsMinStake( +func (k Keeper) IsOptedIn( ctx sdk.Context, - minStake uint64, + consumerId string, providerAddr types.ProviderConsAddress, ) bool { - if minStake == 0 { - return true - } - - validator, err := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.Address) - if err != nil { - k.Logger(ctx).Error("could not retrieve validator by consensus address", "consensus address", providerAddr, "error", err) - return false - } - - // validator has enough stake to validate the chain - return validator.GetBondedTokens().GTE(math.NewIntFromUint64(minStake)) + store := ctx.KVStore(k.storeKey) + return store.Get(types.OptedInKey(consumerId, providerAddr)) != nil } -// ComputeNextValidators computes the validators for the upcoming epoch based on the currently `bondedValidators`. -func (k Keeper) ComputeNextValidators( +// GetAllOptedIn returns all the opted-in validators on chain `consumerId` +func (k Keeper) GetAllOptedIn( ctx sdk.Context, consumerId string, - bondedValidators []stakingtypes.Validator, - powerShapingParameters types.PowerShapingParameters, - minPowerToOptIn int64, -) []types.ConsensusValidator { - // sort the bonded validators by number of staked tokens in descending order - sort.Slice(bondedValidators, func(i, j int) bool { - return bondedValidators[i].GetBondedTokens().GT(bondedValidators[j].GetBondedTokens()) - }) +) (providerConsAddresses []types.ProviderConsAddress) { + store := ctx.KVStore(k.storeKey) + key := types.StringIdWithLenKey(types.OptedInKeyPrefix(), consumerId) + iterator := storetypes.KVStorePrefixIterator(store, key) + defer iterator.Close() - // if inactive validators are not allowed, only consider the first `MaxProviderConsensusValidators` validators - // since those are the ones that participate in consensus - if !powerShapingParameters.AllowInactiveVals { - // only leave the first MaxProviderConsensusValidators bonded validators - maxProviderConsensusVals := k.GetMaxProviderConsensusValidators(ctx) - if len(bondedValidators) > int(maxProviderConsensusVals) { - bondedValidators = bondedValidators[:maxProviderConsensusVals] - } + for ; iterator.Valid(); iterator.Next() { + providerConsAddresses = append(providerConsAddresses, types.NewProviderConsAddress(iterator.Key()[len(key):])) } - nextValidators := k.FilterValidators(ctx, consumerId, bondedValidators, - func(providerAddr types.ProviderConsAddress) bool { - return k.CanValidateChain(ctx, consumerId, providerAddr, powerShapingParameters.Top_N, minPowerToOptIn) && - k.FulfillsMinStake(ctx, powerShapingParameters.MinStake, providerAddr) - }) - - nextValidators = k.CapValidatorSet(ctx, powerShapingParameters, nextValidators) - return k.CapValidatorsPower(ctx, powerShapingParameters.ValidatorsPowerCap, nextValidators) + return providerConsAddresses } -// HasMinPower returns true if the `providerAddr` voting power is GTE than the given minimum power -func (k Keeper) HasMinPower(ctx sdk.Context, providerAddr types.ProviderConsAddress, minPower int64) bool { - val, err := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.Address) - if err != nil { - k.Logger(ctx).Error( - "cannot get last validator power", - "provider address", - providerAddr, - "error", - err, - ) - return false - } +// DeleteAllOptedIn deletes all the opted-in validators for chain with `consumerId` +func (k Keeper) DeleteAllOptedIn( + ctx sdk.Context, + consumerId string, +) { + store := ctx.KVStore(k.storeKey) + key := types.StringIdWithLenKey(types.OptedInKeyPrefix(), consumerId) + iterator := storetypes.KVStorePrefixIterator(store, key) - valAddr, err := sdk.ValAddressFromBech32(val.GetOperator()) - if err != nil { - k.Logger(ctx).Error( - "could not retrieve validator address", - "operator address", - val.GetOperator(), - "error", - err, - ) - return false + var keysToDel [][]byte + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + keysToDel = append(keysToDel, iterator.Key()) } - - power, err := k.stakingKeeper.GetLastValidatorPower(ctx, valAddr) - if err != nil { - k.Logger(ctx).Error("could not retrieve last power of validator address", - "operator address", - val.GetOperator(), - "error", - err, - ) - return false + for _, delKey := range keysToDel { + store.Delete(delKey) } - - return power >= minPower } diff --git a/x/ccv/provider/keeper/partial_set_security_test.go b/x/ccv/provider/keeper/partial_set_security_test.go index c344a01ee1..878e72a642 100644 --- a/x/ccv/provider/keeper/partial_set_security_test.go +++ b/x/ccv/provider/keeper/partial_set_security_test.go @@ -2,27 +2,19 @@ package keeper_test import ( "bytes" - "fmt" - gomath "math" "sort" "testing" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "pgregory.net/rapid" - - "cosmossdk.io/math" codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/cometbft/cometbft/proto/tendermint/crypto" - testkeeper "github.com/cosmos/interchain-security/v6/testutil/keeper" - "github.com/cosmos/interchain-security/v6/x/ccv/provider/keeper" - "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" + providertypes "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" ccvtypes "github.com/cosmos/interchain-security/v6/x/ccv/types" ) @@ -30,28 +22,21 @@ func TestHandleOptIn(t *testing.T) { providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() - providerAddr := types.NewProviderConsAddress([]byte("providerAddr")) + providerAddr := providertypes.NewProviderConsAddress([]byte("providerAddr")) // trying to opt in to an unknown chain require.Error(t, providerKeeper.HandleOptIn(ctx, "unknownConsumerId", providerAddr, "")) // trying to opt in to a stopped consumer chain - providerKeeper.SetConsumerPhase(ctx, "stoppedConsumerId", types.CONSUMER_PHASE_STOPPED) + providerKeeper.SetConsumerPhase(ctx, "stoppedConsumerId", providertypes.CONSUMER_PHASE_STOPPED) require.Error(t, providerKeeper.HandleOptIn(ctx, "stoppedConsumerId", providerAddr, "")) - providerKeeper.SetConsumerPhase(ctx, CONSUMER_ID, types.CONSUMER_PHASE_INITIALIZED) + providerKeeper.SetConsumerPhase(ctx, CONSUMER_ID, providertypes.CONSUMER_PHASE_INITIALIZED) providerKeeper.SetConsumerChainId(ctx, CONSUMER_ID, "chainId") require.False(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, providerAddr)) err := providerKeeper.HandleOptIn(ctx, CONSUMER_ID, providerAddr, "") require.NoError(t, err) require.True(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, providerAddr)) - - // validator tries to opt in to another chain with chain id ("chainId") while it is already opted in to - // a different chain with the same chain id - providerKeeper.SetConsumerPhase(ctx, "consumerId2", types.CONSUMER_PHASE_INITIALIZED) - providerKeeper.SetConsumerChainId(ctx, "consumerId2", "chainId") - err = providerKeeper.HandleOptIn(ctx, "consumerId2", providerAddr, "") - require.ErrorContains(t, err, "validator is already opted in to a chain") } func TestHandleOptInWithConsumerKey(t *testing.T) { @@ -61,7 +46,7 @@ func TestHandleOptInWithConsumerKey(t *testing.T) { // generate a consensus public key for the provider providerConsPubKey := ed25519.GenPrivKeyFromSecret([]byte{1}).PubKey() consAddr := sdk.ConsAddress(providerConsPubKey.Address()) - providerAddr := types.NewProviderConsAddress(consAddr) + providerAddr := providertypes.NewProviderConsAddress(consAddr) calls := []*gomock.Call{ mocks.MockStakingKeeper.EXPECT(). @@ -87,7 +72,7 @@ func TestHandleOptInWithConsumerKey(t *testing.T) { expectedConsumerPubKey, err := providerKeeper.ParseConsumerKey(consumerKey) require.NoError(t, err) - providerKeeper.SetConsumerPhase(ctx, CONSUMER_ID, types.CONSUMER_PHASE_INITIALIZED) + providerKeeper.SetConsumerPhase(ctx, CONSUMER_ID, providertypes.CONSUMER_PHASE_INITIALIZED) providerKeeper.SetConsumerChainId(ctx, CONSUMER_ID, CONSUMER_CHAIN_ID) err = providerKeeper.HandleOptIn(ctx, CONSUMER_ID, providerAddr, consumerKey) require.NoError(t, err) @@ -99,7 +84,7 @@ func TestHandleOptInWithConsumerKey(t *testing.T) { // assert that the `consumerAddr` to `providerAddr` association was set as well consumerAddr, _ := ccvtypes.TMCryptoPublicKeyToConsAddr(actualConsumerPubKey) - actualProviderConsAddr, found := providerKeeper.GetValidatorByConsumerAddr(ctx, CONSUMER_ID, types.NewConsumerConsAddress(consumerAddr)) + actualProviderConsAddr, found := providerKeeper.GetValidatorByConsumerAddr(ctx, CONSUMER_ID, providertypes.NewConsumerConsAddress(consumerAddr)) require.True(t, found) require.Equal(t, providerAddr, actualProviderConsAddr) } @@ -110,21 +95,19 @@ func TestHandleOptOut(t *testing.T) { consumerId := CONSUMER_ID - providerAddr := types.NewProviderConsAddress([]byte("providerAddr")) + providerAddr := providertypes.NewProviderConsAddress([]byte("providerAddr")) // trying to opt out from a not running chain returns an error require.Error(t, providerKeeper.HandleOptOut(ctx, "unknownChainID", providerAddr)) // set the phase and power shaping params - providerKeeper.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED) - err := providerKeeper.SetConsumerPowerShapingParameters(ctx, consumerId, types.PowerShapingParameters{}) + providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + err := providerKeeper.SetConsumerPowerShapingParameters(ctx, consumerId, providertypes.PowerShapingParameters{}) require.NoError(t, err) // if validator (`providerAddr`) is already opted in, then an opt-out would remove this validator providerKeeper.SetOptedIn(ctx, consumerId, providerAddr) require.True(t, providerKeeper.IsOptedIn(ctx, consumerId, providerAddr)) - err = providerKeeper.AppendOptedInConsumerId(ctx, providerAddr, consumerId) - require.NoError(t, err) err = providerKeeper.HandleOptOut(ctx, consumerId, providerAddr) require.NoError(t, err) require.False(t, providerKeeper.IsOptedIn(ctx, consumerId, providerAddr)) @@ -137,11 +120,11 @@ func TestHandleOptOutFromTopNChain(t *testing.T) { consumerId := CONSUMER_ID // set the phase - providerKeeper.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED) + providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) // set the chain as Top 50 and create 4 validators with 10%, 20%, 30%, and 40% of the total voting power // respectively - err := providerKeeper.SetConsumerPowerShapingParameters(ctx, consumerId, types.PowerShapingParameters{ + err := providerKeeper.SetConsumerPowerShapingParameters(ctx, consumerId, providertypes.PowerShapingParameters{ Top_N: 50, }) require.NoError(t, err) @@ -166,27 +149,19 @@ func TestHandleOptOutFromTopNChain(t *testing.T) { providerKeeper.SetMinimumPowerInTopN(ctx, consumerId, minPowerInTopN) // opt in all validators - providerKeeper.SetOptedIn(ctx, consumerId, types.NewProviderConsAddress(valAConsAddr)) - err = providerKeeper.AppendOptedInConsumerId(ctx, types.NewProviderConsAddress(valAConsAddr), consumerId) - require.NoError(t, err) - providerKeeper.SetOptedIn(ctx, consumerId, types.NewProviderConsAddress(valBConsAddr)) - err = providerKeeper.AppendOptedInConsumerId(ctx, types.NewProviderConsAddress(valBConsAddr), consumerId) - require.NoError(t, err) - providerKeeper.SetOptedIn(ctx, consumerId, types.NewProviderConsAddress(valCConsAddr)) - err = providerKeeper.AppendOptedInConsumerId(ctx, types.NewProviderConsAddress(valCConsAddr), consumerId) - require.NoError(t, err) - providerKeeper.SetOptedIn(ctx, consumerId, types.NewProviderConsAddress(valDConsAddr)) - err = providerKeeper.AppendOptedInConsumerId(ctx, types.NewProviderConsAddress(valDConsAddr), consumerId) - require.NoError(t, err) + providerKeeper.SetOptedIn(ctx, consumerId, providertypes.NewProviderConsAddress(valAConsAddr)) + providerKeeper.SetOptedIn(ctx, consumerId, providertypes.NewProviderConsAddress(valBConsAddr)) + providerKeeper.SetOptedIn(ctx, consumerId, providertypes.NewProviderConsAddress(valCConsAddr)) + providerKeeper.SetOptedIn(ctx, consumerId, providertypes.NewProviderConsAddress(valDConsAddr)) // validators A and B can opt out because they belong the bottom 30% of validators - require.NoError(t, providerKeeper.HandleOptOut(ctx, consumerId, types.NewProviderConsAddress(valAConsAddr))) - require.NoError(t, providerKeeper.HandleOptOut(ctx, consumerId, types.NewProviderConsAddress(valBConsAddr))) + require.NoError(t, providerKeeper.HandleOptOut(ctx, consumerId, providertypes.NewProviderConsAddress(valAConsAddr))) + require.NoError(t, providerKeeper.HandleOptOut(ctx, consumerId, providertypes.NewProviderConsAddress(valBConsAddr))) // validators C and D cannot opt out because C has 30% of the voting power and D has 40% of the voting power // and hence both are needed to keep validating a Top 50 chain - require.Error(t, providerKeeper.HandleOptOut(ctx, consumerId, types.NewProviderConsAddress(valCConsAddr))) - require.Error(t, providerKeeper.HandleOptOut(ctx, consumerId, types.NewProviderConsAddress(valDConsAddr))) + require.Error(t, providerKeeper.HandleOptOut(ctx, consumerId, providertypes.NewProviderConsAddress(valCConsAddr))) + require.Error(t, providerKeeper.HandleOptOut(ctx, consumerId, providertypes.NewProviderConsAddress(valDConsAddr))) // opting out a validator that cannot be found from a Top N chain should also return an error notFoundValidator := createStakingValidator(ctx, mocks, 5, 5) @@ -194,54 +169,7 @@ func TestHandleOptOutFromTopNChain(t *testing.T) { require.NoError(t, err) mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(ctx, notFoundValidatorConsAddr). Return(stakingtypes.Validator{}, stakingtypes.ErrNoValidatorFound) - require.Error(t, providerKeeper.HandleOptOut(ctx, consumerId, types.NewProviderConsAddress(notFoundValidatorConsAddr))) -} - -func TestHandleSetConsumerCommissionRate(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - providerAddr := types.NewProviderConsAddress([]byte("providerAddr")) - - // trying to set a commission rate to a unknown consumer chain - require.Error(t, providerKeeper.HandleSetConsumerCommissionRate(ctx, "unknownChainID", providerAddr, math.LegacyZeroDec())) - - // setup a pending consumer chain - consumerId := "0" - providerKeeper.FetchAndIncrementConsumerId(ctx) - providerKeeper.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_INITIALIZED) - - // check that there's no commission rate set for the validator yet - _, found := providerKeeper.GetConsumerCommissionRate(ctx, consumerId, providerAddr) - require.False(t, found) - - mocks.MockStakingKeeper.EXPECT().MinCommissionRate(ctx).Return(math.LegacyZeroDec(), nil).Times(1) - require.NoError(t, providerKeeper.HandleSetConsumerCommissionRate(ctx, consumerId, providerAddr, math.LegacyOneDec())) - - // check that the commission rate is now set - cr, found := providerKeeper.GetConsumerCommissionRate(ctx, consumerId, providerAddr) - require.Equal(t, math.LegacyOneDec(), cr) - require.True(t, found) - - // set minimum rate of 1/2 - commissionRate := math.LegacyNewDec(1).Quo(math.LegacyNewDec(2)) - mocks.MockStakingKeeper.EXPECT().MinCommissionRate(ctx).Return(commissionRate, nil).AnyTimes() - - // try to set a rate slightly below the minimum - require.Error(t, providerKeeper.HandleSetConsumerCommissionRate( - ctx, - consumerId, - providerAddr, - commissionRate.Sub(math.LegacyNewDec(1).Quo(math.LegacyNewDec(100)))), // 0.5 - 0.01 - "commission rate should be rejected (below min), but is not", - ) - - // set a valid commission equal to the minimum - require.NoError(t, providerKeeper.HandleSetConsumerCommissionRate(ctx, consumerId, providerAddr, commissionRate)) - // check that the rate was set - cr, found = providerKeeper.GetConsumerCommissionRate(ctx, consumerId, providerAddr) - require.Equal(t, commissionRate, cr) - require.True(t, found) + require.Error(t, providerKeeper.HandleOptOut(ctx, consumerId, providertypes.NewProviderConsAddress(notFoundValidatorConsAddr))) } func TestOptInTopNValidators(t *testing.T) { @@ -260,16 +188,16 @@ func TestOptInTopNValidators(t *testing.T) { // Start Test 1: opt in all validators with power >= 0 providerKeeper.OptInTopNValidators(ctx, CONSUMER_ID, []stakingtypes.Validator{valA, valB, valC, valD}, 0) - expectedOptedInValidators := []types.ProviderConsAddress{ - types.NewProviderConsAddress(valAConsAddr), - types.NewProviderConsAddress(valBConsAddr), - types.NewProviderConsAddress(valCConsAddr), - types.NewProviderConsAddress(valDConsAddr), + expectedOptedInValidators := []providertypes.ProviderConsAddress{ + providertypes.NewProviderConsAddress(valAConsAddr), + providertypes.NewProviderConsAddress(valBConsAddr), + providertypes.NewProviderConsAddress(valCConsAddr), + providertypes.NewProviderConsAddress(valDConsAddr), } actualOptedInValidators := providerKeeper.GetAllOptedIn(ctx, CONSUMER_ID) // sort validators first to be able to compare - sortUpdates := func(addresses []types.ProviderConsAddress) { + sortUpdates := func(addresses []providertypes.ProviderConsAddress) { sort.Slice(addresses, func(i, j int) bool { return bytes.Compare(addresses[i].ToSdkConsAddr(), addresses[j].ToSdkConsAddr()) < 0 }) @@ -280,10 +208,10 @@ func TestOptInTopNValidators(t *testing.T) { require.Equal(t, expectedOptedInValidators, actualOptedInValidators) // reset state for the upcoming checks - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valAConsAddr)) - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valBConsAddr)) - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valCConsAddr)) - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valDConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valAConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valBConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valCConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valDConsAddr)) // Start Test 2: opt in all validators with power >= 1 // We expect the same `expectedOptedInValidators` as when we opted in all validators with power >= 0 because the @@ -293,16 +221,16 @@ func TestOptInTopNValidators(t *testing.T) { sortUpdates(actualOptedInValidators) require.Equal(t, expectedOptedInValidators, actualOptedInValidators) - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valAConsAddr)) - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valBConsAddr)) - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valCConsAddr)) - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valDConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valAConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valBConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valCConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valDConsAddr)) // Start Test 3: opt in all validators with power >= 2 and hence we do not expect to opt in validator A providerKeeper.OptInTopNValidators(ctx, CONSUMER_ID, []stakingtypes.Validator{valA, valB, valC, valD}, 2) - expectedOptedInValidators = []types.ProviderConsAddress{ - types.NewProviderConsAddress(valBConsAddr), - types.NewProviderConsAddress(valCConsAddr), + expectedOptedInValidators = []providertypes.ProviderConsAddress{ + providertypes.NewProviderConsAddress(valBConsAddr), + providertypes.NewProviderConsAddress(valCConsAddr), } actualOptedInValidators = providerKeeper.GetAllOptedIn(ctx, CONSUMER_ID) @@ -312,664 +240,62 @@ func TestOptInTopNValidators(t *testing.T) { require.Equal(t, expectedOptedInValidators, actualOptedInValidators) // reset state for the upcoming checks - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valAConsAddr)) - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valBConsAddr)) - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valCConsAddr)) - providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, types.NewProviderConsAddress(valDConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valAConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valBConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valCConsAddr)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, providertypes.NewProviderConsAddress(valDConsAddr)) // Start Test 4: opt in all validators with power >= 4 and hence we do not expect any opted-in validators providerKeeper.OptInTopNValidators(ctx, CONSUMER_ID, []stakingtypes.Validator{valA, valB, valC, valD}, 4) require.Empty(t, providerKeeper.GetAllOptedIn(ctx, CONSUMER_ID)) } -func TestComputeMinPowerInTopN(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - // create 5 validators with powers 1, 3, 5, 6, 10 (not in that order) with total power of 25 (= 1 + 3 + 5 + 6 + 10) - // such that: - // validator power => cumulative share - // 10 => 40% - // 6 => 64% - // 5 => 84% - // 3 => 96% - // 1 => 100% - - bondedValidators := []stakingtypes.Validator{ - createStakingValidator(ctx, mocks, 5, 1), - createStakingValidator(ctx, mocks, 10, 2), - createStakingValidator(ctx, mocks, 3, 3), - createStakingValidator(ctx, mocks, 1, 4), - createStakingValidator(ctx, mocks, 6, 5), - } - - m, err := providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 100) - require.NoError(t, err) - require.Equal(t, int64(1), m) - - m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 97) - require.NoError(t, err) - require.Equal(t, int64(1), m) - - m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 96) - require.NoError(t, err) - require.Equal(t, int64(3), m) - - m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 85) - require.NoError(t, err) - require.Equal(t, int64(3), m) - - m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 84) - require.NoError(t, err) - require.Equal(t, int64(5), m) - - m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 65) - require.NoError(t, err) - require.Equal(t, int64(5), m) - - m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 64) - require.NoError(t, err) - require.Equal(t, int64(6), m) - - m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 50) - require.NoError(t, err) - require.Equal(t, int64(6), m) - - m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 40) - require.NoError(t, err) - require.Equal(t, int64(10), m) - - m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 1) - require.NoError(t, err) - require.Equal(t, int64(10), m) - - _, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 0) - require.Error(t, err) - - _, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 101) - require.Error(t, err) -} - -// TestCanValidateChain returns true if `validator` is opted in, in `consumerId. -func TestCanValidateChain(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - consumerID := "0" - - validator := createStakingValidator(ctx, mocks, 1, 1) // adds GetLastValidatorPower expectation to mocks - consAddr, _ := validator.GetConsAddr() - providerAddr := types.NewProviderConsAddress(consAddr) - - // with no allowlist or denylist, the validator has to be opted in, in order to consider it - powerShapingParameters, err := providerKeeper.GetConsumerPowerShapingParameters(ctx, consumerID) - require.Error(t, err) - require.False(t, providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 0)) - - // with TopN chains, the validator can be considered, - mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), providerAddr.Address).Return(validator, nil).Times(2) - err = providerKeeper.SetConsumerPowerShapingParameters(ctx, consumerID, types.PowerShapingParameters{Top_N: 50}) - require.NoError(t, err) - powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, consumerID) - require.NoError(t, err) - // validator's power is LT the min power - require.False(t, providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 2)) - // validator's power is GTE the min power - require.True(t, providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 1)) - - // when validator is opted-in it can validate regardless of its min power - providerKeeper.SetOptedIn(ctx, consumerID, types.NewProviderConsAddress(consAddr)) - require.True(t, providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 2)) - - // With OptIn chains, validator can validate only if it has already opted-in - err = providerKeeper.SetConsumerPowerShapingParameters(ctx, consumerID, types.PowerShapingParameters{Top_N: 0}) - require.NoError(t, err) - powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, consumerID) - require.NoError(t, err) - require.True(t, providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 2)) - - // create an allow list but do not add the validator `providerAddr` to it - validatorA := createStakingValidator(ctx, mocks, 1, 2) - consAddrA, _ := validatorA.GetConsAddr() - providerKeeper.SetAllowlist(ctx, consumerID, types.NewProviderConsAddress(consAddrA)) - require.False(t, providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 1)) - providerKeeper.SetAllowlist(ctx, consumerID, types.NewProviderConsAddress(consAddr)) - require.True(t, providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 1)) - - // create a denylist but do not add validator `providerAddr` to it - providerKeeper.SetDenylist(ctx, consumerID, types.NewProviderConsAddress(consAddrA)) - require.True(t, providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 1)) - // add validator `providerAddr` to the denylist - providerKeeper.SetDenylist(ctx, consumerID, types.NewProviderConsAddress(consAddr)) - require.False(t, providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 1)) -} - -func TestCapValidatorSet(t *testing.T) { +func TestGetAllOptedIn(t *testing.T) { providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() - validatorA := types.ConsensusValidator{ - ProviderConsAddr: []byte("providerConsAddrA"), - Power: 1, - PublicKey: &crypto.PublicKey{}, - } - - validatorB := types.ConsensusValidator{ - ProviderConsAddr: []byte("providerConsAddrB"), - Power: 2, - PublicKey: &crypto.PublicKey{}, - } - - validatorC := types.ConsensusValidator{ - ProviderConsAddr: []byte("providerConsAddrC"), - Power: 3, - PublicKey: &crypto.PublicKey{}, - } - validators := []types.ConsensusValidator{validatorA, validatorB, validatorC} - - powerShapingParameters, err := providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) - require.Error(t, err) - consumerValidators := providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) - require.Equal(t, validators, consumerValidators) - - err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, types.PowerShapingParameters{ - ValidatorSetCap: 0, - }) - require.NoError(t, err) - powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) - require.NoError(t, err) - consumerValidators = providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) - require.Equal(t, validators, consumerValidators) - - err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, types.PowerShapingParameters{ - ValidatorSetCap: 100, - }) - require.NoError(t, err) - powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) - require.NoError(t, err) - consumerValidators = providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) - require.Equal(t, validators, consumerValidators) - - err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, types.PowerShapingParameters{ - ValidatorSetCap: 1, - }) - require.NoError(t, err) - powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) - require.NoError(t, err) - consumerValidators = providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) - require.Equal(t, []types.ConsensusValidator{validatorC}, consumerValidators) - - err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, types.PowerShapingParameters{ - ValidatorSetCap: 2, - }) - require.NoError(t, err) - powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) - require.NoError(t, err) - consumerValidators = providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) - require.Equal(t, []types.ConsensusValidator{validatorC, validatorB}, consumerValidators) - - err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, types.PowerShapingParameters{ - ValidatorSetCap: 3, - }) - require.NoError(t, err) - powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) - require.NoError(t, err) - consumerValidators = providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) - require.Equal(t, []types.ConsensusValidator{validatorC, validatorB, validatorA}, consumerValidators) -} - -func TestCapValidatorsPower(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - validatorA := types.ConsensusValidator{ - ProviderConsAddr: []byte("providerConsAddrA"), - Power: 1, - PublicKey: &crypto.PublicKey{}, - } - - validatorB := types.ConsensusValidator{ - ProviderConsAddr: []byte("providerConsAddrB"), - Power: 2, - PublicKey: &crypto.PublicKey{}, - } - - validatorC := types.ConsensusValidator{ - ProviderConsAddr: []byte("providerConsAddrC"), - Power: 3, - PublicKey: &crypto.PublicKey{}, - } - - validatorD := types.ConsensusValidator{ - ProviderConsAddr: []byte("providerConsAddrD"), - Power: 4, - PublicKey: &crypto.PublicKey{}, - } - - validators := []types.ConsensusValidator{validatorA, validatorB, validatorC, validatorD} - - expectedValidators := make([]types.ConsensusValidator, len(validators)) - copy(expectedValidators, validators) - expectedValidators[0].Power = 2 - expectedValidators[1].Power = 2 - expectedValidators[2].Power = 3 - expectedValidators[3].Power = 3 - - sortValidators := func(validators []types.ConsensusValidator) { - sort.Slice(validators, func(i, j int) bool { - return bytes.Compare(validators[i].ProviderConsAddr, validators[j].ProviderConsAddr) < 0 - }) - } - - // no capping takes place because validators power-cap is not set - powerShapingParameters, err := providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) - require.Error(t, err) - cappedValidators := providerKeeper.CapValidatorsPower(ctx, powerShapingParameters.ValidatorsPowerCap, validators) - sortValidators(validators) - sortValidators(cappedValidators) - require.Equal(t, validators, cappedValidators) - - err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, types.PowerShapingParameters{ - ValidatorsPowerCap: 33, - }) - require.NoError(t, err) - powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) - require.NoError(t, err) - cappedValidators = providerKeeper.CapValidatorsPower(ctx, powerShapingParameters.ValidatorsPowerCap, validators) - sortValidators(expectedValidators) - sortValidators(cappedValidators) - require.Equal(t, expectedValidators, cappedValidators) -} - -func TestNoMoreThanPercentOfTheSum(t *testing.T) { - // **impossible** case where we only have 9 powers, and we want that no number has more than 10% of the total sum - powers := []int64{1, 2, 3, 4, 5, 6, 7, 8, 9} - percent := uint32(10) - require.False(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) - - powers = []int64{1, 2, 3, 4, 5} - percent = 20 - require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) - - powers = []int64{1, 2, 3, 4, 5} - percent = 21 - require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) - - powers = []int64{1, 2, 3, 4, 5} - percent = 25 - require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) - - powers = []int64{1, 2, 3, 4, 5} - percent = 32 - require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) - - powers = []int64{1, 2, 3, 4, 5} - percent = 33 - require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) - - powers = []int64{1, 2, 3, 4, 5} - percent = 34 - require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) - - powers = []int64{1, 2, 3, 4, 5} - percent = 50 - require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) -} - -func createConsumerValidators(powers []int64) []types.ConsensusValidator { - var validators []types.ConsensusValidator - for _, p := range powers { - validators = append(validators, types.ConsensusValidator{ - ProviderConsAddr: []byte("providerConsAddr"), - Power: p, - PublicKey: &crypto.PublicKey{}, - }) - } - return validators -} - -// returns `true` if no validator in `validators` corresponds to more than `percent` of the total sum of all -// validators' powers -func noMoreThanPercent(validators []types.ConsensusValidator, percent uint32) bool { - sum := int64(0) - for _, v := range validators { - sum += v.Power - } - - for _, v := range validators { - if float64(v.Power)*100.0 > float64(percent)*float64(sum) { - return false - } - } - return true -} - -func sumPowers(vals []types.ConsensusValidator) int64 { - sum := int64(0) - for _, v := range vals { - sum += v.Power - } - return sum -} - -func CapSatisfiable(vals []types.ConsensusValidator, percent uint32) bool { - // 100 / len(vals) is what each validator gets if each has the same power. - // if this is more than the cap, it cannot be satisfied. - return float64(100)/float64(len(vals)) < float64(percent) -} - -func TestNoMoreThanPercentOfTheSumProps(t *testing.T) { - // define properties to test - - // capRespectedIfSatisfiable: if the cap can be respected, then it will be respected - capRespectedIfSatisfiable := func(valsBefore, valsAfter []types.ConsensusValidator, percent uint32) bool { - if CapSatisfiable(valsBefore, percent) { - return noMoreThanPercent(valsAfter, percent) - } - return true + expectedOptedInValidators := []providertypes.ProviderConsAddress{ + providertypes.NewProviderConsAddress([]byte("providerAddr1")), + providertypes.NewProviderConsAddress([]byte("providerAddr2")), + providertypes.NewProviderConsAddress([]byte("providerAddr3")), } - evenPowersIfCapCannotBeSatisfied := func(valsBefore, valsAfter []types.ConsensusValidator, percent uint32) bool { - if !CapSatisfiable(valsBefore, percent) { - // if the cap cannot be satisfied, each validator should have the same power - for _, valAfter := range valsAfter { - if valAfter.Power != valsAfter[0].Power { - return false - } - } - } - return true - } - - // fairness: if before, v1 has more power than v2, then afterwards v1 will not have less power than v2 - // (they might get the same power if they are both capped) - fairness := func(valsBefore, valsAfter []types.ConsensusValidator) bool { - for i, v := range valsBefore { - // find the validator after with the same address - vAfter := findConsumerValidator(t, v, valsAfter) - - // go through all other validators before (after this one, to avoid double checking) - for j := i + 1; j < len(valsBefore); j++ { - otherV := valsBefore[j] - otherVAfter := findConsumerValidator(t, otherV, valsAfter) - - // v has at least as much power before - if v.Power >= otherV.Power { - // then otherV should not have more power after - if vAfter.Power < otherVAfter.Power { - return false - } - } else { - // v has less power before - // then v should not have more power after - if vAfter.Power > otherVAfter.Power { - return false - } - } - } - } - return true + for _, expectedOptedInValidator := range expectedOptedInValidators { + providerKeeper.SetOptedIn(ctx, CONSUMER_ID, expectedOptedInValidator) } - // non-zero: v has non-zero power before IFF it has non-zero power after - nonZero := func(valsBefore, valsAfter []types.ConsensusValidator) bool { - for _, v := range valsBefore { - vAfter := findConsumerValidator(t, v, valsAfter) - if (v.Power == 0) != (vAfter.Power == 0) { - return false - } - } - return true - } - - // equalSumIfCapSatisfiable: the sum of the powers of the validators will not change if the cap can be satisfied - // (except for small changes by rounding errors) - equalSumIfCapSatisfiable := func(valsBefore, valsAfter []types.ConsensusValidator, percent uint32) bool { - if CapSatisfiable(valsBefore, percent) { - difference := gomath.Abs(float64(sumPowers(valsBefore) - sumPowers(valsAfter))) - if difference > 1 { - // if the difference is more than a rounding error, they are not equal - return false - } - } - return true - } - - // num validators: the number of validators will not change - equalNumVals := func(valsBefore, valsAfter []types.ConsensusValidator) bool { - return len(valsBefore) == len(valsAfter) - } - - // test setup for pbt - rapid.Check(t, func(t *rapid.T) { - powers := rapid.SliceOf(rapid.Int64Range(1, 1000000000000)).Draw(t, "powers") - percent := uint32(rapid.Int32Range(1, 100).Draw(t, "percent")) - - consumerValidators := createConsumerValidators(powers) - cappedValidators := keeper.NoMoreThanPercentOfTheSum(consumerValidators, percent) - - t.Log("can the cap be satisfied: ", CapSatisfiable(consumerValidators, percent)) - t.Log("before: ", consumerValidators) - t.Log("after: ", cappedValidators) - - // check properties - require.True(t, capRespectedIfSatisfiable(consumerValidators, cappedValidators, percent)) - require.True(t, evenPowersIfCapCannotBeSatisfied(consumerValidators, cappedValidators, percent)) - require.True(t, fairness(consumerValidators, cappedValidators)) - require.True(t, nonZero(consumerValidators, cappedValidators)) - require.True(t, equalSumIfCapSatisfiable(consumerValidators, cappedValidators, percent), "sum before: %v, sum after: %v", sumPowers(consumerValidators), sumPowers(cappedValidators)) - require.True(t, equalNumVals(consumerValidators, cappedValidators), "num before: %v, num after: %v", len(consumerValidators), len(cappedValidators)) - }) -} - -func findConsumerValidator(t *testing.T, v types.ConsensusValidator, valsAfter []types.ConsensusValidator) *types.ConsensusValidator { - t.Helper() - - index := -1 - for i, vA := range valsAfter { - if bytes.Equal(v.ProviderConsAddr, vA.ProviderConsAddr) { - index = i - break - } - } - if index == -1 { - t.Fatalf("could not find validator with address %v in validators after \n validators after capping: %v", v.ProviderConsAddr, valsAfter) - } - return &valsAfter[index] -} - -func createStakingValidatorsAndMocks(ctx sdk.Context, mocks testkeeper.MockedKeepers, powers ...int64) ([]stakingtypes.Validator, []types.ProviderConsAddress) { - var validators []stakingtypes.Validator - for i, power := range powers { - val := createStakingValidator(ctx, mocks, power, i) - val.Tokens = math.NewInt(power) - val.Status = stakingtypes.Bonded - validators = append(validators, val) - } - - var consAddrs []types.ProviderConsAddress - for _, val := range validators { - consAddr, err := val.GetConsAddr() - if err != nil { - panic(err) - } - consAddrs = append(consAddrs, types.NewProviderConsAddress(consAddr)) - } - // set up mocks - for index, val := range validators { - mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(ctx, consAddrs[index].Address).Return(val, nil).AnyTimes() - } - - return validators, consAddrs -} - -// TestFulfillsMinStake checks that FulfillsMinStake returns true if the validator has at least the min stake -// and false otherwise -func TestFulfillsMinStake(t *testing.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - // create two validators with powers 1 and 2 - _, consAddrs := createStakingValidatorsAndMocks(ctx, mocks, 1, 2) - - testCases := []struct { - name string - minStake uint64 - expectedFulfill []bool - }{ - { - name: "No min stake", - minStake: 0, - expectedFulfill: []bool{true, true}, - }, - { - name: "Min stake set to 2", - minStake: 2, - expectedFulfill: []bool{false, true}, - }, - { - name: "Min stake set to 3", - minStake: 3, - expectedFulfill: []bool{false, false}, - }, - } + actualOptedInValidators := providerKeeper.GetAllOptedIn(ctx, CONSUMER_ID) - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - for i, valAddr := range consAddrs { - result := providerKeeper.FulfillsMinStake(ctx, tc.minStake, valAddr) - require.Equal(t, tc.expectedFulfill[i], result) - } + // sort validators first to be able to compare + sortOptedInValidators := func(addresses []providertypes.ProviderConsAddress) { + sort.Slice(addresses, func(i, j int) bool { + return bytes.Compare(addresses[i].ToSdkConsAddr(), addresses[j].ToSdkConsAddr()) < 0 }) } + sortOptedInValidators(expectedOptedInValidators) + sortOptedInValidators(actualOptedInValidators) + require.Equal(t, expectedOptedInValidators, actualOptedInValidators) } -// TestIfInactiveValsDisallowedProperty checks that the number of validators in the next validator set is at most -// the MaxProviderConsensusValidators parameter if the consumer chain does not allow inactive validators to validate. -func TestIfInactiveValsDisallowedProperty(t *testing.T) { - rapid.Check(t, func(r *rapid.T) { - providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - // Generate a random number of validators with random powers - valPowers := rapid.SliceOfN(rapid.Int64Range(1, 100), 1, 100).Draw(r, "valPowers") - vals, consAddrs := createStakingValidatorsAndMocks(ctx, mocks, valPowers...) - - // opt the validators in - for _, valAddr := range consAddrs { - providerKeeper.SetOptedIn(ctx, CONSUMER_ID, valAddr) - } - - // Randomly choose values for parameters - minStake := rapid.Uint64Range(0, 101).Draw(r, "minStake") - maxProviderConsensusVals := rapid.Uint32Range(1, 11).Draw(r, "maxProviderConsensusVals") - - // Set up the parameters in the provider keeper - - // do not allow inactive validators - err := providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, types.PowerShapingParameters{ - MinStake: minStake, - AllowInactiveVals: false, - }) - require.NoError(t, err) - params := providerKeeper.GetParams(ctx) - params.MaxProviderConsensusValidators = int64(maxProviderConsensusVals) - providerKeeper.SetParams(ctx, params) - - powerShapingParameters, err := providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) - require.NoError(t, err) - - // Compute the next validators - nextVals := providerKeeper.ComputeNextValidators(ctx, CONSUMER_ID, vals, powerShapingParameters, 0) - - // Check that the length of nextVals is at most maxProviderConsensusVals - require.LessOrEqual(r, len(nextVals), int(maxProviderConsensusVals), "The length of nextVals should be at most maxProviderConsensusVals") - - // Sanity check: we only get 0 next validators if either: - // - maxProviderConsensusVals is 0 - // - the maximal validator power is less than the min stake - if len(nextVals) == 0 { - maxValPower := int64(0) - for _, power := range valPowers { - if power > maxValPower { - maxValPower = power - } - } - require.True( - r, - maxProviderConsensusVals == 0 || maxValPower < int64(minStake), - "The length of nextVals should only be 0 if either maxProviderConsensusVals is 0 or the maximal validator power is less than the min stake", - ) - } - }) -} - -func TestHasMinPower(t *testing.T) { - pk, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) +// TestOptedIn tests the `SetOptedIn`, `IsOptedIn`, `DeleteOptedIn` and `DeleteAllOptedIn` methods +func TestOptedIn(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() - providerConsPubKey := ed25519.GenPrivKeyFromSecret([]byte{1}).PubKey() - consAddr := sdk.ConsAddress(providerConsPubKey.Address()) - providerAddr := types.NewProviderConsAddress(consAddr) - - testCases := []struct { - name string - minPower uint64 - expectation func(sdk.ConsAddress, testkeeper.MockedKeepers) - hasMinPower bool - }{ - { - name: "cannot find validator by cons address", - expectation: func(sdk.ConsAddress, testkeeper.MockedKeepers) { - mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), consAddr). - Return(stakingtypes.Validator{}, fmt.Errorf("validator not found")).Times(1) - }, - hasMinPower: false, - }, { - name: "cannot convert validator operator address", - expectation: func(consAddr sdk.ConsAddress, mocks testkeeper.MockedKeepers) { - mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), consAddr).Return(stakingtypes.Validator{OperatorAddress: "xxxx"}, nil).Times(1) - }, - hasMinPower: false, - }, { - name: "cannot find last validator power", - expectation: func(consAddr sdk.ConsAddress, mocks testkeeper.MockedKeepers) { - valAddr := sdk.ValAddress(providerAddr.Address.Bytes()) - mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), consAddr). - Return(stakingtypes.Validator{OperatorAddress: valAddr.String()}, nil) - mocks.MockStakingKeeper.EXPECT().GetLastValidatorPower(gomock.Any(), valAddr). - Return(int64(0), fmt.Errorf("last power not found")).Times(1) - }, - hasMinPower: false, - }, { - name: "validator power is LT min power", - expectation: func(consAddr sdk.ConsAddress, mocks testkeeper.MockedKeepers) { - valAddr := sdk.ValAddress(providerAddr.Address.Bytes()) - mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), consAddr). - Return(stakingtypes.Validator{OperatorAddress: valAddr.String()}, nil) - mocks.MockStakingKeeper.EXPECT().GetLastValidatorPower(gomock.Any(), valAddr). - Return(int64(5), nil).Times(1) - }, - hasMinPower: false, - }, { - name: "validator power is GTE min power", - expectation: func(consAddr sdk.ConsAddress, mocks testkeeper.MockedKeepers) { - valAddr := sdk.ValAddress(providerAddr.Address.Bytes()) - mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), consAddr). - Return(stakingtypes.Validator{OperatorAddress: valAddr.String()}, nil) - mocks.MockStakingKeeper.EXPECT().GetLastValidatorPower(gomock.Any(), valAddr). - Return(int64(10), nil).Times(1) - }, - hasMinPower: true, - }, - } - - minPower := int64(10) - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - tc.expectation(consAddr, mocks) - require.Equal(t, tc.hasMinPower, pk.HasMinPower(ctx, providerAddr, minPower)) - }) - } + optedInValidator1 := providertypes.NewProviderConsAddress([]byte("providerAddr1")) + optedInValidator2 := providertypes.NewProviderConsAddress([]byte("providerAddr2")) + + require.False(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator1)) + providerKeeper.SetOptedIn(ctx, CONSUMER_ID, optedInValidator1) + require.True(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator1)) + providerKeeper.DeleteOptedIn(ctx, CONSUMER_ID, optedInValidator1) + require.False(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator1)) + + providerKeeper.SetOptedIn(ctx, CONSUMER_ID, optedInValidator1) + providerKeeper.SetOptedIn(ctx, CONSUMER_ID, optedInValidator2) + require.True(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator1)) + require.True(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator2)) + providerKeeper.DeleteAllOptedIn(ctx, CONSUMER_ID) + require.False(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator1)) + require.False(t, providerKeeper.IsOptedIn(ctx, CONSUMER_ID, optedInValidator2)) } diff --git a/x/ccv/provider/keeper/permissionless.go b/x/ccv/provider/keeper/permissionless.go index ee4272be1f..f096234d2a 100644 --- a/x/ccv/provider/keeper/permissionless.go +++ b/x/ccv/provider/keeper/permissionless.go @@ -176,115 +176,3 @@ func (k Keeper) IsConsumerActive(ctx sdk.Context, consumerId string) bool { phase == types.CONSUMER_PHASE_INITIALIZED || phase == types.CONSUMER_PHASE_LAUNCHED } - -// GetOptedInConsumerIds returns all the consumer ids where the given validator is opted in -func (k Keeper) GetOptedInConsumerIds(ctx sdk.Context, providerAddr types.ProviderConsAddress) (types.ConsumerIds, error) { - store := ctx.KVStore(k.storeKey) - bz := store.Get(types.ProviderConsAddrToOptedInConsumerIdsKey(providerAddr)) - if bz == nil { - return types.ConsumerIds{}, nil - } - - var consumerIds types.ConsumerIds - if err := consumerIds.Unmarshal(bz); err != nil { - return types.ConsumerIds{}, fmt.Errorf("failed to unmarshal consumer ids: %w", err) - } - - return consumerIds, nil -} - -// AppendOptedInConsumerId appends given consumer id to the list of consumers that validator has opted in -// TODO (PERMISSIONLESS) -- combine it with SetOptedIn -func (k Keeper) AppendOptedInConsumerId(ctx sdk.Context, providerAddr types.ProviderConsAddress, consumerId string) error { - store := ctx.KVStore(k.storeKey) - - consumers, err := k.GetOptedInConsumerIds(ctx, providerAddr) - if err != nil { - return err - } - - consumersWithAppend := types.ConsumerIds{ - Ids: append(consumers.Ids, consumerId), - } - - bz, err := consumersWithAppend.Marshal() - if err != nil { - return err - } - - store.Set(types.ProviderConsAddrToOptedInConsumerIdsKey(providerAddr), bz) - return nil -} - -// RemoveOptedInConsumerId removes the consumer id from this validator because it is not opted in anymore -func (k Keeper) RemoveOptedInConsumerId(ctx sdk.Context, providerAddr types.ProviderConsAddress, consumerId string) error { - store := ctx.KVStore(k.storeKey) - - consumers, err := k.GetOptedInConsumerIds(ctx, providerAddr) - if err != nil { - return err - } - - if len(consumers.Ids) == 0 { - return fmt.Errorf("no consumer ids found for this provviderAddr: %s", providerAddr.String()) - } - - // find the index of the consumer we want to remove - index := -1 - for i := 0; i < len(consumers.Ids); i++ { - if consumers.Ids[i] == consumerId { - index = i - break - } - } - - if index == -1 { - return fmt.Errorf("failed to find consumer id (%s)", consumerId) - } - - if len(consumers.Ids) == 1 { - store.Delete(types.ProviderConsAddrToOptedInConsumerIdsKey(providerAddr)) - return nil - } - - consumersWithRemoval := types.ConsumerIds{ - Ids: append(consumers.Ids[:index], consumers.Ids[index+1:]...), - } - - bz, err := consumersWithRemoval.Marshal() - if err != nil { - return err - } - - store.Set(types.ProviderConsAddrToOptedInConsumerIdsKey(providerAddr), bz) - return nil -} - -// IsValidatorOptedInToChainId checks if the validator with `providerAddr` is opted into the chain with the specified `chainId`. -// It returns `found == true` and the corresponding chain's `consumerId` if the validator is opted in. Otherwise, it returns an empty string -// for `consumerId` and `found == false`. -func (k Keeper) IsValidatorOptedInToChainId(ctx sdk.Context, providerAddr types.ProviderConsAddress, chainId string) (string, bool) { - consumers, err := k.GetOptedInConsumerIds(ctx, providerAddr) - if err != nil { - k.Logger(ctx).Error("failed to retrieve the consumer ids this validator is opted in to", - "providerAddr", providerAddr, - "error", err) - return "", false - } - - for _, consumerId := range consumers.Ids { - consumerChainId, err := k.GetConsumerChainId(ctx, consumerId) - if err != nil { - k.Logger(ctx).Error("cannot find chain id", - "consumerId", consumerId, - "error", err) - continue - } - - if consumerChainId == chainId { - return consumerId, true - } - - } - return "", false -} diff --git a/x/ccv/provider/keeper/permissionless_test.go b/x/ccv/provider/keeper/permissionless_test.go index 195fdfa405..145afb43f1 100644 --- a/x/ccv/provider/keeper/permissionless_test.go +++ b/x/ccv/provider/keeper/permissionless_test.go @@ -196,75 +196,3 @@ func TestConsumerPhase(t *testing.T) { phase = providerKeeper.GetConsumerPhase(ctx, CONSUMER_ID) require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) } - -// TestOptedInConsumerIds tests the `GetOptedInConsumerIds`, `AppendOptedInConsumerId`, and `RemoveOptedInConsumerId` methods -func TestGetOptedInConsumerIds(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - providerAddr := providertypes.NewProviderConsAddress([]byte("providerAddr")) - consumers, err := providerKeeper.GetOptedInConsumerIds(ctx, providerAddr) - require.NoError(t, err) - require.Empty(t, consumers) - - err = providerKeeper.AppendOptedInConsumerId(ctx, providerAddr, "consumerId1") - require.NoError(t, err) - consumers, err = providerKeeper.GetOptedInConsumerIds(ctx, providerAddr) - require.NoError(t, err) - require.Equal(t, providertypes.ConsumerIds{ - Ids: []string{"consumerId1"}, - }, consumers) - - err = providerKeeper.AppendOptedInConsumerId(ctx, providerAddr, "consumerId2") - require.NoError(t, err) - consumers, err = providerKeeper.GetOptedInConsumerIds(ctx, providerAddr) - require.NoError(t, err) - require.Equal(t, providertypes.ConsumerIds{ - Ids: []string{"consumerId1", "consumerId2"}, - }, consumers) - - err = providerKeeper.AppendOptedInConsumerId(ctx, providerAddr, "consumerId3") - require.NoError(t, err) - consumers, err = providerKeeper.GetOptedInConsumerIds(ctx, providerAddr) - require.NoError(t, err) - require.Equal(t, providertypes.ConsumerIds{ - Ids: []string{"consumerId1", "consumerId2", "consumerId3"}, - }, consumers) - - // remove all the consumer ids - err = providerKeeper.RemoveOptedInConsumerId(ctx, providerAddr, "consumerId2") - require.NoError(t, err) - consumers, err = providerKeeper.GetOptedInConsumerIds(ctx, providerAddr) - require.NoError(t, err) - require.Equal(t, providertypes.ConsumerIds{ - Ids: []string{"consumerId1", "consumerId3"}, - }, consumers) - - err = providerKeeper.RemoveOptedInConsumerId(ctx, providerAddr, "consumerId3") - require.NoError(t, err) - - err = providerKeeper.RemoveOptedInConsumerId(ctx, providerAddr, "consumerId1") - require.NoError(t, err) - - consumers, err = providerKeeper.GetOptedInConsumerIds(ctx, providerAddr) - require.NoError(t, err) - require.Empty(t, consumers) -} - -func TestIsValidatorOptedInToChain(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) - defer ctrl.Finish() - - chainId := "chainId" - providerAddr := providertypes.NewProviderConsAddress([]byte("providerAddr")) - _, found := providerKeeper.IsValidatorOptedInToChainId(ctx, providerAddr, chainId) - require.False(t, found) - - expectedConsumerId := CONSUMER_ID - providerKeeper.SetConsumerChainId(ctx, expectedConsumerId, chainId) - providerKeeper.SetOptedIn(ctx, expectedConsumerId, providerAddr) - providerKeeper.AppendOptedInConsumerId(ctx, providerAddr, expectedConsumerId) - actualConsumerId, found := providerKeeper.IsValidatorOptedInToChainId(ctx, providerAddr, chainId) - require.True(t, found) - require.Equal(t, expectedConsumerId, actualConsumerId) -} diff --git a/x/ccv/provider/keeper/power_shaping.go b/x/ccv/provider/keeper/power_shaping.go index 40aad77aa7..87624922b0 100644 --- a/x/ccv/provider/keeper/power_shaping.go +++ b/x/ccv/provider/keeper/power_shaping.go @@ -4,16 +4,305 @@ import ( "encoding/binary" "errors" "fmt" + "sort" errorsmod "cosmossdk.io/errors" + "cosmossdk.io/math" storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" ccvtypes "github.com/cosmos/interchain-security/v6/x/ccv/types" ) +// ComputeMinPowerInTopN returns the minimum power needed for a validator (from the bonded validators) +// to belong to the `topN`% of validators for a Top N chain. +func (k Keeper) ComputeMinPowerInTopN(ctx sdk.Context, bondedValidators []stakingtypes.Validator, topN uint32) (int64, error) { + if topN == 0 || topN > 100 { + // Note that Top N chains have a lower limit on `topN`, namely that topN cannot be less than 50. + // However, we can envision that this method could be used for other (future) reasons where this might not + // be the case. For this, this method operates for `topN`s in (0, 100]. + return 0, fmt.Errorf("trying to compute minimum power with an incorrect"+ + " topN value (%d). topN has to be in (0, 100]", topN) + } + + totalPower := math.LegacyZeroDec() + var powers []int64 + + for _, val := range bondedValidators { + valAddr, err := sdk.ValAddressFromBech32(val.GetOperator()) + if err != nil { + return 0, err + } + power, err := k.stakingKeeper.GetLastValidatorPower(ctx, valAddr) + if err != nil { + return 0, err + } + powers = append(powers, power) + totalPower = totalPower.Add(math.LegacyNewDec(power)) + } + + // sort by powers descending + sort.Slice(powers, func(i, j int) bool { + return powers[i] > powers[j] + }) + + topNThreshold := math.LegacyNewDec(int64(topN)).QuoInt64(int64(100)) + powerSum := math.LegacyZeroDec() + for _, power := range powers { + powerSum = powerSum.Add(math.LegacyNewDec(power)) + if powerSum.Quo(totalPower).GTE(topNThreshold) { + return power, nil + } + } + + // We should never reach this point because the topN can be up to 1.0 (100%) and in the above `for` loop we + // perform an equal comparison as well (`GTE`). + return 0, fmt.Errorf("should never reach this point with topN (%d), totalPower (%d), and powerSum (%d)", topN, totalPower, powerSum) +} + +// UpdateMinimumPowerInTopN populates the minimum power in Top N for the consumer chain with this consumer id +func (k Keeper) UpdateMinimumPowerInTopN(ctx sdk.Context, consumerId string, oldTopN, newTopN uint32) error { + // if the top N changes, we need to update the new minimum power in top N + if newTopN != oldTopN { + if newTopN > 0 { + // if the chain receives a non-zero top N value, store the minimum power in the top N + bondedValidators, err := k.GetLastProviderConsensusActiveValidators(ctx) + if err != nil { + return err + } + minPower, err := k.ComputeMinPowerInTopN(ctx, bondedValidators, newTopN) + if err != nil { + return err + } + k.SetMinimumPowerInTopN(ctx, consumerId, minPower) + } else { + // if the chain receives a zero top N value, we delete the min power + k.DeleteMinimumPowerInTopN(ctx, consumerId) + } + } + + return nil +} + +// CapValidatorSet caps the provided `validators` if chain with `consumerId` is an Opt In chain with a validator-set cap. +// If cap is `k`, `CapValidatorSet` returns the first `k` validators from `validators` with the highest power. +func (k Keeper) CapValidatorSet( + ctx sdk.Context, + powerShapingParameters types.PowerShapingParameters, + validators []types.ConsensusValidator, +) []types.ConsensusValidator { + if powerShapingParameters.Top_N > 0 { + // is a no-op if the chain is a Top N chain + return validators + } + + validatorSetCap := powerShapingParameters.ValidatorSetCap + if validatorSetCap != 0 && int(validatorSetCap) < len(validators) { + sort.Slice(validators, func(i, j int) bool { + return validators[i].Power > validators[j].Power + }) + + return validators[:int(validatorSetCap)] + } else { + return validators + } +} + +// CapValidatorsPower caps the power of the validators on chain with `consumerId` and returns an updated slice of validators +// with their new powers. Works on a best-basis effort because there are cases where we cannot guarantee that all validators +// on the consumer chain have less power than the set validators-power cap. For example, if we have 10 validators and +// the power cap is set to 5%, we need at least one validator to have more than 10% of the voting power on the consumer chain. +func (k Keeper) CapValidatorsPower( + ctx sdk.Context, + validatorsPowerCap uint32, + validators []types.ConsensusValidator, +) []types.ConsensusValidator { + if validatorsPowerCap > 0 { + return NoMoreThanPercentOfTheSum(validators, validatorsPowerCap) + } else { + // is a no-op if power cap is not set for `consumerId` + return validators + } +} + +// sum is a helper function to sum all the validators' power +func sum(validators []types.ConsensusValidator) int64 { + s := int64(0) + for _, v := range validators { + s += v.Power + } + return s +} + +// NoMoreThanPercentOfTheSum returns a set of validators with updated powers such that no validator has more than the +// provided `percent` of the sum of all validators' powers. Operates on a best-effort basis. +func NoMoreThanPercentOfTheSum(validators []types.ConsensusValidator, percent uint32) []types.ConsensusValidator { + // Algorithm's idea + // ---------------- + // Consider the validators' powers to be `a_1, a_2, ... a_n` and `p` to be the percent in [1, 100]. Now, consider + // the sum `s = a_1 + a_2 + ... + a_n`. Then `maxPower = s * p / 100 <=> 100 * maxPower = s * p`. + // The problem of capping the validators' powers to be no more than `p` has no solution if `(100 / n) > p`. For example, + // for n = 10 and p = 5 we do not have a solution. We would need at least one validator with power greater than 10% + // for a solution to exist. + // So, if `(100 / n) > p` there's no solution. We know that `100 * maxPower = s * p` and so `(100 / n) > (100 * maxPower) / s` + // `100 * s > 100 * maxPower * n <=> s > maxPower * n`. Thus, we do not have a solution if `s > n * maxPower`. + + // If `s <= n * maxPower` the idea of the algorithm is rather simple. + // - Compute the `maxPower` a validator must have so that it does not have more than `percent` of the voting power. + // - If a validator `v` has power `p`, then: + // - if `p > maxPower` we set `v`'s power to `maxPower` and distribute the `p - maxPower` to validators that + // have less than `maxPower` power. This way, the total sum remains the same and no validator has more than + // `maxPower` and so the power capping is satisfied. + // - Note that in order to avoid setting multiple validators to `maxPower`, we first compute all the `remainingPower` + // we have to distribute and then attempt to add `remainingPower / validatorsWithPowerLessThanMaxPower` to each validator. + // To guarantee that the sum remains the same after the distribution of powers, we sort the powers in descending + // order. This way, we first attempt to add `remainingPower / validatorsWithPowerLessThanMaxPower` to validators + // with greater power and if we cannot add the `remainingPower / validatorsWithPowerLessThanMaxPower` without + // exceeding `maxPower`, we just add enough to reach `maxPower` and add the remaining power to validators with smaller + // power. + + // If `s > n * maxPower` there's no solution and the algorithm would set everything to `maxPower`. + // ---------------- + + // Computes `floor((sum(validators) * percent) / 100)` + maxPower := math.LegacyNewDec(sum(validators)).Mul(math.LegacyNewDec(int64(percent))).QuoInt64(100).TruncateInt64() + + if maxPower == 0 { + // edge case: set `maxPower` to 1 to avoid setting the power of a validator to 0 + maxPower = 1 + } + + // Sort by `.Power` in decreasing order. Sorting in descending order is needed because otherwise we would have cases + // like this `powers =[60, 138, 559]` and `p = 35%` where sum is `757` and `maxValue = 264`. + // Because `559 - 264 = 295` we have to distribute 295 to the first 2 numbers (`295/2 = 147` to each number). However, + // note that `138 + 147 > 264`. If we were to add 147 to 60 first, then we cannot give the remaining 147 to 138. + // So, the idea is to first give `126 (= 264 - 138)` to 138, so it becomes 264, and then add `21 (=147 - 26) + 147` to 60. + sort.Slice(validators, func(i, j int) bool { + return validators[i].Power > validators[j].Power + }) + + // `remainingPower` is to be distributed to validators that have power less than `maxPower` + remainingPower := int64(0) + validatorsWithPowerLessThanMaxPower := 0 + for _, v := range validators { + if v.Power >= maxPower { + remainingPower += (v.Power - maxPower) + } else { + validatorsWithPowerLessThanMaxPower++ + } + } + + updatedValidators := make([]types.ConsensusValidator, len(validators)) + + powerPerValidator := int64(0) + remainingValidators := int64(validatorsWithPowerLessThanMaxPower) + if remainingValidators != 0 { + // power to give to each validator in order to distribute the `remainingPower` + powerPerValidator = remainingPower / remainingValidators + } + + for i, v := range validators { + if v.Power >= maxPower { + updatedValidators[i] = validators[i] + updatedValidators[i].Power = maxPower + } else if v.Power+powerPerValidator >= maxPower { + updatedValidators[i] = validators[i] + updatedValidators[i].Power = maxPower + remainingPower -= (maxPower - v.Power) + remainingValidators-- + } else { + updatedValidators[i] = validators[i] + updatedValidators[i].Power = v.Power + powerPerValidator + remainingPower -= (updatedValidators[i].Power - validators[i].Power) + remainingValidators-- + } + if remainingValidators == 0 { + continue + } + powerPerValidator = remainingPower / remainingValidators + } + + return updatedValidators +} + +// CanValidateChain returns true if the validator `providerAddr` is opted-in to chain with `consumerId` and the allowlist +// and denylist do not prevent the validator from validating the chain. +func (k Keeper) CanValidateChain( + ctx sdk.Context, + consumerId string, + providerAddr types.ProviderConsAddress, + topN uint32, + minPowerToOptIn int64, +) (bool, error) { + // check if the validator is already opted-in + optedIn := k.IsOptedIn(ctx, consumerId, providerAddr) + + // check if the validator is automatically opted-in for a topN chain + if !optedIn && topN > 0 { + var err error + optedIn, err = k.HasMinPower(ctx, providerAddr, minPowerToOptIn) + if err != nil { + return false, err + } + } + + // only consider opted-in validators + return optedIn && + // if an allowlist is declared, only consider allowlisted validators + (k.IsAllowlistEmpty(ctx, consumerId) || + k.IsAllowlisted(ctx, consumerId, providerAddr)) && + // if a denylist is declared, only consider denylisted validators + (k.IsDenylistEmpty(ctx, consumerId) || + !k.IsDenylisted(ctx, consumerId, providerAddr)), nil +} + +// FulfillsMinStake returns true if the validator `providerAddr` has enough stake to validate chain with `consumerId` +// by checking its staked tokens against the minimum stake required to validate the chain. +func (k Keeper) FulfillsMinStake( + ctx sdk.Context, + minStake uint64, + providerAddr types.ProviderConsAddress, +) (bool, error) { + if minStake == 0 { + return true, nil + } + + validator, err := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.Address) + if err != nil { + return false, err + } + + // validator has enough stake to validate the chain + return validator.GetBondedTokens().GTE(math.NewIntFromUint64(minStake)), nil +} + +// HasMinPower returns true if the `providerAddr` voting power is GTE than the given minimum power +func (k Keeper) HasMinPower(ctx sdk.Context, providerAddr types.ProviderConsAddress, minPower int64) (bool, error) { + val, err := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.Address) + if err != nil { + return false, err + } + + valAddr, err := sdk.ValAddressFromBech32(val.GetOperator()) + if err != nil { + return false, err + } + + power, err := k.stakingKeeper.GetLastValidatorPower(ctx, valAddr) + if err != nil { + return false, err + } + + return power >= minPower, nil +} + +// +// Setter and getters +// + // GetConsumerPowerShapingParameters returns the power-shaping parameters associated with this consumer id func (k Keeper) GetConsumerPowerShapingParameters(ctx sdk.Context, consumerId string) (types.PowerShapingParameters, error) { store := ctx.KVStore(k.storeKey) @@ -262,27 +551,3 @@ func (k Keeper) DeleteMinimumPowerInTopN( store := ctx.KVStore(k.storeKey) store.Delete(types.MinimumPowerInTopNKey(consumerId)) } - -// UpdateMinimumPowerInTopN populates the minimum power in Top N for the consumer chain with this consumer id -func (k Keeper) UpdateMinimumPowerInTopN(ctx sdk.Context, consumerId string, oldTopN, newTopN uint32) error { - // if the top N changes, we need to update the new minimum power in top N - if newTopN != oldTopN { - if newTopN > 0 { - // if the chain receives a non-zero top N value, store the minimum power in the top N - bondedValidators, err := k.GetLastProviderConsensusActiveValidators(ctx) - if err != nil { - return err - } - minPower, err := k.ComputeMinPowerInTopN(ctx, bondedValidators, newTopN) - if err != nil { - return err - } - k.SetMinimumPowerInTopN(ctx, consumerId, minPower) - } else { - // if the chain receives a zero top N value, we delete the min power - k.DeleteMinimumPowerInTopN(ctx, consumerId) - } - } - - return nil -} diff --git a/x/ccv/provider/keeper/power_shaping_test.go b/x/ccv/provider/keeper/power_shaping_test.go index faf72ce5a5..b0853588ee 100644 --- a/x/ccv/provider/keeper/power_shaping_test.go +++ b/x/ccv/provider/keeper/power_shaping_test.go @@ -3,20 +3,711 @@ package keeper_test import ( "bytes" "errors" + "fmt" + gomath "math" "sort" "testing" + "cosmossdk.io/math" + "github.com/cometbft/cometbft/proto/tendermint/crypto" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + "pgregory.net/rapid" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" testkeeper "github.com/cosmos/interchain-security/v6/testutil/keeper" + "github.com/cosmos/interchain-security/v6/x/ccv/provider/keeper" providertypes "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" ccvtypes "github.com/cosmos/interchain-security/v6/x/ccv/types" ) +func TestHasMinPower(t *testing.T) { + pk, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + providerConsPubKey := ed25519.GenPrivKeyFromSecret([]byte{1}).PubKey() + consAddr := sdk.ConsAddress(providerConsPubKey.Address()) + providerAddr := providertypes.NewProviderConsAddress(consAddr) + + testCases := []struct { + name string + minPower uint64 + expectation func(sdk.ConsAddress, testkeeper.MockedKeepers) + expError bool + hasMinPower bool + }{ + { + name: "cannot find validator by cons address", + expectation: func(sdk.ConsAddress, testkeeper.MockedKeepers) { + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), consAddr). + Return(stakingtypes.Validator{}, fmt.Errorf("validator not found")).Times(1) + }, + expError: true, + hasMinPower: false, + }, { + name: "cannot convert validator operator address", + expectation: func(consAddr sdk.ConsAddress, mocks testkeeper.MockedKeepers) { + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), consAddr).Return(stakingtypes.Validator{OperatorAddress: "xxxx"}, nil).Times(1) + }, + expError: true, + hasMinPower: false, + }, { + name: "cannot find last validator power", + expectation: func(consAddr sdk.ConsAddress, mocks testkeeper.MockedKeepers) { + valAddr := sdk.ValAddress(providerAddr.Address.Bytes()) + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), consAddr). + Return(stakingtypes.Validator{OperatorAddress: valAddr.String()}, nil) + mocks.MockStakingKeeper.EXPECT().GetLastValidatorPower(gomock.Any(), valAddr). + Return(int64(0), fmt.Errorf("last power not found")).Times(1) + }, + expError: true, + hasMinPower: false, + }, { + name: "validator power is LT min power", + expectation: func(consAddr sdk.ConsAddress, mocks testkeeper.MockedKeepers) { + valAddr := sdk.ValAddress(providerAddr.Address.Bytes()) + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), consAddr). + Return(stakingtypes.Validator{OperatorAddress: valAddr.String()}, nil) + mocks.MockStakingKeeper.EXPECT().GetLastValidatorPower(gomock.Any(), valAddr). + Return(int64(5), nil).Times(1) + }, + expError: false, + hasMinPower: false, + }, { + name: "validator power is GTE min power", + expectation: func(consAddr sdk.ConsAddress, mocks testkeeper.MockedKeepers) { + valAddr := sdk.ValAddress(providerAddr.Address.Bytes()) + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), consAddr). + Return(stakingtypes.Validator{OperatorAddress: valAddr.String()}, nil) + mocks.MockStakingKeeper.EXPECT().GetLastValidatorPower(gomock.Any(), valAddr). + Return(int64(10), nil).Times(1) + }, + expError: false, + hasMinPower: true, + }, + } + + minPower := int64(10) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.expectation(consAddr, mocks) + hasMinPower, err := pk.HasMinPower(ctx, providerAddr, minPower) + if tc.expError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, tc.hasMinPower, hasMinPower) + }) + } +} + +func TestComputeMinPowerInTopN(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // create 5 validators with powers 1, 3, 5, 6, 10 (not in that order) with total power of 25 (= 1 + 3 + 5 + 6 + 10) + // such that: + // validator power => cumulative share + // 10 => 40% + // 6 => 64% + // 5 => 84% + // 3 => 96% + // 1 => 100% + + bondedValidators := []stakingtypes.Validator{ + createStakingValidator(ctx, mocks, 5, 1), + createStakingValidator(ctx, mocks, 10, 2), + createStakingValidator(ctx, mocks, 3, 3), + createStakingValidator(ctx, mocks, 1, 4), + createStakingValidator(ctx, mocks, 6, 5), + } + + m, err := providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 100) + require.NoError(t, err) + require.Equal(t, int64(1), m) + + m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 97) + require.NoError(t, err) + require.Equal(t, int64(1), m) + + m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 96) + require.NoError(t, err) + require.Equal(t, int64(3), m) + + m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 85) + require.NoError(t, err) + require.Equal(t, int64(3), m) + + m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 84) + require.NoError(t, err) + require.Equal(t, int64(5), m) + + m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 65) + require.NoError(t, err) + require.Equal(t, int64(5), m) + + m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 64) + require.NoError(t, err) + require.Equal(t, int64(6), m) + + m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 50) + require.NoError(t, err) + require.Equal(t, int64(6), m) + + m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 40) + require.NoError(t, err) + require.Equal(t, int64(10), m) + + m, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 1) + require.NoError(t, err) + require.Equal(t, int64(10), m) + + _, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 0) + require.Error(t, err) + + _, err = providerKeeper.ComputeMinPowerInTopN(ctx, bondedValidators, 101) + require.Error(t, err) +} + +// TestCanValidateChain returns true if `validator` is opted in, in `consumerId. +func TestCanValidateChain(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + consumerID := "0" + + validator := createStakingValidator(ctx, mocks, 1, 1) // adds GetLastValidatorPower expectation to mocks + consAddr, _ := validator.GetConsAddr() + providerAddr := providertypes.NewProviderConsAddress(consAddr) + + // with no allowlist or denylist, the validator has to be opted in, in order to consider it + powerShapingParameters, err := providerKeeper.GetConsumerPowerShapingParameters(ctx, consumerID) + require.Error(t, err) + canValidateChain, err := providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 0) + require.NoError(t, err) + require.False(t, canValidateChain) + + // with TopN chains, the validator can be considered, + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(gomock.Any(), providerAddr.Address).Return(validator, nil).Times(2) + err = providerKeeper.SetConsumerPowerShapingParameters(ctx, consumerID, providertypes.PowerShapingParameters{Top_N: 50}) + require.NoError(t, err) + powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, consumerID) + require.NoError(t, err) + // validator's power is LT the min power + canValidateChain, err = providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 2) + require.NoError(t, err) + require.False(t, canValidateChain) + // validator's power is GTE the min power + canValidateChain, err = providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 1) + require.NoError(t, err) + require.True(t, canValidateChain) + + // when validator is opted-in it can validate regardless of its min power + providerKeeper.SetOptedIn(ctx, consumerID, providertypes.NewProviderConsAddress(consAddr)) + canValidateChain, err = providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 2) + require.NoError(t, err) + require.True(t, canValidateChain) + + // With OptIn chains, validator can validate only if it has already opted-in + err = providerKeeper.SetConsumerPowerShapingParameters(ctx, consumerID, providertypes.PowerShapingParameters{Top_N: 0}) + require.NoError(t, err) + powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, consumerID) + require.NoError(t, err) + canValidateChain, err = providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 2) + require.NoError(t, err) + require.True(t, canValidateChain) + + // create an allow list but do not add the validator `providerAddr` to it + validatorA := createStakingValidator(ctx, mocks, 1, 2) + consAddrA, _ := validatorA.GetConsAddr() + providerKeeper.SetAllowlist(ctx, consumerID, providertypes.NewProviderConsAddress(consAddrA)) + canValidateChain, err = providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 1) + require.NoError(t, err) + require.False(t, canValidateChain) + providerKeeper.SetAllowlist(ctx, consumerID, providertypes.NewProviderConsAddress(consAddr)) + canValidateChain, err = providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 1) + require.NoError(t, err) + require.True(t, canValidateChain) + + // create a denylist but do not add validator `providerAddr` to it + providerKeeper.SetDenylist(ctx, consumerID, providertypes.NewProviderConsAddress(consAddrA)) + canValidateChain, err = providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 1) + require.NoError(t, err) + require.True(t, canValidateChain) + // add validator `providerAddr` to the denylist + providerKeeper.SetDenylist(ctx, consumerID, providertypes.NewProviderConsAddress(consAddr)) + canValidateChain, err = providerKeeper.CanValidateChain(ctx, consumerID, providerAddr, powerShapingParameters.Top_N, 1) + require.NoError(t, err) + require.False(t, canValidateChain) +} + +func TestCapValidatorSet(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + validatorA := providertypes.ConsensusValidator{ + ProviderConsAddr: []byte("providerConsAddrA"), + Power: 1, + PublicKey: &crypto.PublicKey{}, + } + + validatorB := providertypes.ConsensusValidator{ + ProviderConsAddr: []byte("providerConsAddrB"), + Power: 2, + PublicKey: &crypto.PublicKey{}, + } + + validatorC := providertypes.ConsensusValidator{ + ProviderConsAddr: []byte("providerConsAddrC"), + Power: 3, + PublicKey: &crypto.PublicKey{}, + } + validators := []providertypes.ConsensusValidator{validatorA, validatorB, validatorC} + + powerShapingParameters, err := providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) + require.Error(t, err) + consumerValidators := providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) + require.Equal(t, validators, consumerValidators) + + err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, providertypes.PowerShapingParameters{ + ValidatorSetCap: 0, + }) + require.NoError(t, err) + powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) + require.NoError(t, err) + consumerValidators = providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) + require.Equal(t, validators, consumerValidators) + + err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, providertypes.PowerShapingParameters{ + ValidatorSetCap: 100, + }) + require.NoError(t, err) + powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) + require.NoError(t, err) + consumerValidators = providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) + require.Equal(t, validators, consumerValidators) + + err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, providertypes.PowerShapingParameters{ + ValidatorSetCap: 1, + }) + require.NoError(t, err) + powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) + require.NoError(t, err) + consumerValidators = providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) + require.Equal(t, []providertypes.ConsensusValidator{validatorC}, consumerValidators) + + err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, providertypes.PowerShapingParameters{ + ValidatorSetCap: 2, + }) + require.NoError(t, err) + powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) + require.NoError(t, err) + consumerValidators = providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) + require.Equal(t, []providertypes.ConsensusValidator{validatorC, validatorB}, consumerValidators) + + err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, providertypes.PowerShapingParameters{ + ValidatorSetCap: 3, + }) + require.NoError(t, err) + powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) + require.NoError(t, err) + consumerValidators = providerKeeper.CapValidatorSet(ctx, powerShapingParameters, validators) + require.Equal(t, []providertypes.ConsensusValidator{validatorC, validatorB, validatorA}, consumerValidators) +} + +func TestCapValidatorsPower(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + validatorA := providertypes.ConsensusValidator{ + ProviderConsAddr: []byte("providerConsAddrA"), + Power: 1, + PublicKey: &crypto.PublicKey{}, + } + + validatorB := providertypes.ConsensusValidator{ + ProviderConsAddr: []byte("providerConsAddrB"), + Power: 2, + PublicKey: &crypto.PublicKey{}, + } + + validatorC := providertypes.ConsensusValidator{ + ProviderConsAddr: []byte("providerConsAddrC"), + Power: 3, + PublicKey: &crypto.PublicKey{}, + } + + validatorD := providertypes.ConsensusValidator{ + ProviderConsAddr: []byte("providerConsAddrD"), + Power: 4, + PublicKey: &crypto.PublicKey{}, + } + + validators := []providertypes.ConsensusValidator{validatorA, validatorB, validatorC, validatorD} + + expectedValidators := make([]providertypes.ConsensusValidator, len(validators)) + copy(expectedValidators, validators) + expectedValidators[0].Power = 2 + expectedValidators[1].Power = 2 + expectedValidators[2].Power = 3 + expectedValidators[3].Power = 3 + + sortValidators := func(validators []providertypes.ConsensusValidator) { + sort.Slice(validators, func(i, j int) bool { + return bytes.Compare(validators[i].ProviderConsAddr, validators[j].ProviderConsAddr) < 0 + }) + } + + // no capping takes place because validators power-cap is not set + powerShapingParameters, err := providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) + require.Error(t, err) + cappedValidators := providerKeeper.CapValidatorsPower(ctx, powerShapingParameters.ValidatorsPowerCap, validators) + sortValidators(validators) + sortValidators(cappedValidators) + require.Equal(t, validators, cappedValidators) + + err = providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, providertypes.PowerShapingParameters{ + ValidatorsPowerCap: 33, + }) + require.NoError(t, err) + powerShapingParameters, err = providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) + require.NoError(t, err) + cappedValidators = providerKeeper.CapValidatorsPower(ctx, powerShapingParameters.ValidatorsPowerCap, validators) + sortValidators(expectedValidators) + sortValidators(cappedValidators) + require.Equal(t, expectedValidators, cappedValidators) +} + +func TestNoMoreThanPercentOfTheSum(t *testing.T) { + // **impossible** case where we only have 9 powers, and we want that no number has more than 10% of the total sum + powers := []int64{1, 2, 3, 4, 5, 6, 7, 8, 9} + percent := uint32(10) + require.False(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) + + powers = []int64{1, 2, 3, 4, 5} + percent = 20 + require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) + + powers = []int64{1, 2, 3, 4, 5} + percent = 21 + require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) + + powers = []int64{1, 2, 3, 4, 5} + percent = 25 + require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) + + powers = []int64{1, 2, 3, 4, 5} + percent = 32 + require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) + + powers = []int64{1, 2, 3, 4, 5} + percent = 33 + require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) + + powers = []int64{1, 2, 3, 4, 5} + percent = 34 + require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) + + powers = []int64{1, 2, 3, 4, 5} + percent = 50 + require.True(t, noMoreThanPercent(keeper.NoMoreThanPercentOfTheSum(createConsumerValidators(powers), percent), percent)) +} + +func createConsumerValidators(powers []int64) []providertypes.ConsensusValidator { + var validators []providertypes.ConsensusValidator + for _, p := range powers { + validators = append(validators, providertypes.ConsensusValidator{ + ProviderConsAddr: []byte("providerConsAddr"), + Power: p, + PublicKey: &crypto.PublicKey{}, + }) + } + return validators +} + +// returns `true` if no validator in `validators` corresponds to more than `percent` of the total sum of all +// validators' powers +func noMoreThanPercent(validators []providertypes.ConsensusValidator, percent uint32) bool { + sum := int64(0) + for _, v := range validators { + sum += v.Power + } + + for _, v := range validators { + if float64(v.Power)*100.0 > float64(percent)*float64(sum) { + return false + } + } + return true +} + +func sumPowers(vals []providertypes.ConsensusValidator) int64 { + sum := int64(0) + for _, v := range vals { + sum += v.Power + } + return sum +} + +func CapSatisfiable(vals []providertypes.ConsensusValidator, percent uint32) bool { + // 100 / len(vals) is what each validator gets if each has the same power. + // if this is more than the cap, it cannot be satisfied. + return float64(100)/float64(len(vals)) < float64(percent) +} + +func TestNoMoreThanPercentOfTheSumProps(t *testing.T) { + // define properties to test + + // capRespectedIfSatisfiable: if the cap can be respected, then it will be respected + capRespectedIfSatisfiable := func(valsBefore, valsAfter []providertypes.ConsensusValidator, percent uint32) bool { + if CapSatisfiable(valsBefore, percent) { + return noMoreThanPercent(valsAfter, percent) + } + return true + } + + evenPowersIfCapCannotBeSatisfied := func(valsBefore, valsAfter []providertypes.ConsensusValidator, percent uint32) bool { + if !CapSatisfiable(valsBefore, percent) { + // if the cap cannot be satisfied, each validator should have the same power + for _, valAfter := range valsAfter { + if valAfter.Power != valsAfter[0].Power { + return false + } + } + } + return true + } + + // fairness: if before, v1 has more power than v2, then afterwards v1 will not have less power than v2 + // (they might get the same power if they are both capped) + fairness := func(valsBefore, valsAfter []providertypes.ConsensusValidator) bool { + for i, v := range valsBefore { + // find the validator after with the same address + vAfter := findConsumerValidator(t, v, valsAfter) + + // go through all other validators before (after this one, to avoid double checking) + for j := i + 1; j < len(valsBefore); j++ { + otherV := valsBefore[j] + otherVAfter := findConsumerValidator(t, otherV, valsAfter) + + // v has at least as much power before + if v.Power >= otherV.Power { + // then otherV should not have more power after + if vAfter.Power < otherVAfter.Power { + return false + } + } else { + // v has less power before + // then v should not have more power after + if vAfter.Power > otherVAfter.Power { + return false + } + } + } + } + return true + } + + // non-zero: v has non-zero power before IFF it has non-zero power after + nonZero := func(valsBefore, valsAfter []providertypes.ConsensusValidator) bool { + for _, v := range valsBefore { + vAfter := findConsumerValidator(t, v, valsAfter) + if (v.Power == 0) != (vAfter.Power == 0) { + return false + } + } + return true + } + + // equalSumIfCapSatisfiable: the sum of the powers of the validators will not change if the cap can be satisfied + // (except for small changes by rounding errors) + equalSumIfCapSatisfiable := func(valsBefore, valsAfter []providertypes.ConsensusValidator, percent uint32) bool { + if CapSatisfiable(valsBefore, percent) { + difference := gomath.Abs(float64(sumPowers(valsBefore) - sumPowers(valsAfter))) + if difference > 1 { + // if the difference is more than a rounding error, they are not equal + return false + } + } + return true + } + + // num validators: the number of validators will not change + equalNumVals := func(valsBefore, valsAfter []providertypes.ConsensusValidator) bool { + return len(valsBefore) == len(valsAfter) + } + + // test setup for pbt + rapid.Check(t, func(t *rapid.T) { + powers := rapid.SliceOf(rapid.Int64Range(1, 1000000000000)).Draw(t, "powers") + percent := uint32(rapid.Int32Range(1, 100).Draw(t, "percent")) + + consumerValidators := createConsumerValidators(powers) + cappedValidators := keeper.NoMoreThanPercentOfTheSum(consumerValidators, percent) + + t.Log("can the cap be satisfied: ", CapSatisfiable(consumerValidators, percent)) + t.Log("before: ", consumerValidators) + t.Log("after: ", cappedValidators) + + // check properties + require.True(t, capRespectedIfSatisfiable(consumerValidators, cappedValidators, percent)) + require.True(t, evenPowersIfCapCannotBeSatisfied(consumerValidators, cappedValidators, percent)) + require.True(t, fairness(consumerValidators, cappedValidators)) + require.True(t, nonZero(consumerValidators, cappedValidators)) + require.True(t, equalSumIfCapSatisfiable(consumerValidators, cappedValidators, percent), "sum before: %v, sum after: %v", sumPowers(consumerValidators), sumPowers(cappedValidators)) + require.True(t, equalNumVals(consumerValidators, cappedValidators), "num before: %v, num after: %v", len(consumerValidators), len(cappedValidators)) + }) +} + +func findConsumerValidator(t *testing.T, v providertypes.ConsensusValidator, valsAfter []providertypes.ConsensusValidator) *providertypes.ConsensusValidator { + t.Helper() + + index := -1 + for i, vA := range valsAfter { + if bytes.Equal(v.ProviderConsAddr, vA.ProviderConsAddr) { + index = i + break + } + } + if index == -1 { + t.Fatalf("could not find validator with address %v in validators after \n validators after capping: %v", v.ProviderConsAddr, valsAfter) + } + return &valsAfter[index] +} + +func createStakingValidatorsAndMocks(ctx sdk.Context, mocks testkeeper.MockedKeepers, powers ...int64) ([]stakingtypes.Validator, []providertypes.ProviderConsAddress) { + var validators []stakingtypes.Validator + for i, power := range powers { + val := createStakingValidator(ctx, mocks, power, i) + val.Tokens = math.NewInt(power) + val.Status = stakingtypes.Bonded + validators = append(validators, val) + } + + var consAddrs []providertypes.ProviderConsAddress + for _, val := range validators { + consAddr, err := val.GetConsAddr() + if err != nil { + panic(err) + } + consAddrs = append(consAddrs, providertypes.NewProviderConsAddress(consAddr)) + } + // set up mocks + for index, val := range validators { + mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(ctx, consAddrs[index].Address).Return(val, nil).AnyTimes() + } + + return validators, consAddrs +} + +// TestFulfillsMinStake checks that FulfillsMinStake returns true if the validator has at least the min stake +// and false otherwise +func TestFulfillsMinStake(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // create two validators with powers 1 and 2 + _, consAddrs := createStakingValidatorsAndMocks(ctx, mocks, 1, 2) + + testCases := []struct { + name string + minStake uint64 + expectedFulfill []bool + }{ + { + name: "No min stake", + minStake: 0, + expectedFulfill: []bool{true, true}, + }, + { + name: "Min stake set to 2", + minStake: 2, + expectedFulfill: []bool{false, true}, + }, + { + name: "Min stake set to 3", + minStake: 3, + expectedFulfill: []bool{false, false}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, valAddr := range consAddrs { + result, err := providerKeeper.FulfillsMinStake(ctx, tc.minStake, valAddr) + require.NoError(t, err) + require.Equal(t, tc.expectedFulfill[i], result) + } + }) + } +} + +// TestIfInactiveValsDisallowedProperty checks that the number of validators in the next validator set is at most +// the MaxProviderConsensusValidators parameter if the consumer chain does not allow inactive validators to validate. +func TestIfInactiveValsDisallowedProperty(t *testing.T) { + rapid.Check(t, func(r *rapid.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + // Generate a random number of validators with random powers + valPowers := rapid.SliceOfN(rapid.Int64Range(1, 100), 1, 100).Draw(r, "valPowers") + vals, consAddrs := createStakingValidatorsAndMocks(ctx, mocks, valPowers...) + + // opt the validators in + for _, valAddr := range consAddrs { + providerKeeper.SetOptedIn(ctx, CONSUMER_ID, valAddr) + } + + // Randomly choose values for parameters + minStake := rapid.Uint64Range(0, 101).Draw(r, "minStake") + maxProviderConsensusVals := rapid.Uint32Range(1, 11).Draw(r, "maxProviderConsensusVals") + + // Set up the parameters in the provider keeper + + // do not allow inactive validators + err := providerKeeper.SetConsumerPowerShapingParameters(ctx, CONSUMER_ID, providertypes.PowerShapingParameters{ + MinStake: minStake, + AllowInactiveVals: false, + }) + require.NoError(t, err) + params := providerKeeper.GetParams(ctx) + params.MaxProviderConsensusValidators = int64(maxProviderConsensusVals) + providerKeeper.SetParams(ctx, params) + + powerShapingParameters, err := providerKeeper.GetConsumerPowerShapingParameters(ctx, CONSUMER_ID) + require.NoError(t, err) + + // Compute the next validators + nextVals, err := providerKeeper.ComputeNextValidators(ctx, CONSUMER_ID, vals, powerShapingParameters, 0) + require.NoError(t, err) + + // Check that the length of nextVals is at most maxProviderConsensusVals + require.LessOrEqual(r, len(nextVals), int(maxProviderConsensusVals), "The length of nextVals should be at most maxProviderConsensusVals") + + // Sanity check: we only get 0 next validators if either: + // - maxProviderConsensusVals is 0 + // - the maximal validator power is less than the min stake + if len(nextVals) == 0 { + maxValPower := int64(0) + for _, power := range valPowers { + if power > maxValPower { + maxValPower = power + } + } + require.True( + r, + maxProviderConsensusVals == 0 || maxValPower < int64(minStake), + "The length of nextVals should only be 0 if either maxProviderConsensusVals is 0 or the maximal validator power is less than the min stake", + ) + } + }) +} + // TestConsumerPowerShapingParameters tests the getter and setter of the consumer id to power-shaping parameters methods func TestConsumerPowerShapingParameters(t *testing.T) { providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) diff --git a/x/ccv/provider/keeper/relay.go b/x/ccv/provider/keeper/relay.go index a1a639b275..2a956d2f8f 100644 --- a/x/ccv/provider/keeper/relay.go +++ b/x/ccv/provider/keeper/relay.go @@ -251,7 +251,10 @@ func (k Keeper) QueueVSCPackets(ctx sdk.Context) error { k.OptInTopNValidators(ctx, consumerId, activeValidators, minPower) } - nextValidators := k.ComputeNextValidators(ctx, consumerId, bondedValidators, powerShapingParameters, minPower) + nextValidators, err := k.ComputeNextValidators(ctx, consumerId, bondedValidators, powerShapingParameters, minPower) + if err != nil { + return fmt.Errorf("computing next validators, consumerId(%s), minPower(%d): %w", consumerId, minPower, err) + } valUpdates := DiffValidators(currentValidators, nextValidators) err = k.SetConsumerValSet(ctx, consumerId, nextValidators) diff --git a/x/ccv/provider/keeper/validator_set_update.go b/x/ccv/provider/keeper/validator_set_update.go index 95e833da00..2366568e2e 100644 --- a/x/ccv/provider/keeper/validator_set_update.go +++ b/x/ccv/provider/keeper/validator_set_update.go @@ -2,6 +2,7 @@ package keeper import ( "fmt" + "sort" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -167,8 +168,8 @@ func (k Keeper) FilterValidators( ctx sdk.Context, consumerId string, bondedValidators []stakingtypes.Validator, - predicate func(providerAddr types.ProviderConsAddress) bool, -) []types.ConsensusValidator { + predicate func(providerAddr types.ProviderConsAddress) (bool, error), +) ([]types.ConsensusValidator, error) { var nextValidators []types.ConsensusValidator for _, val := range bondedValidators { consAddr, err := val.GetConsAddr() @@ -176,21 +177,66 @@ func (k Keeper) FilterValidators( continue } - if predicate(types.NewProviderConsAddress(consAddr)) { + ok, err := predicate(types.NewProviderConsAddress(consAddr)) + if err != nil { + return nextValidators, err + } + if ok { nextValidator, err := k.CreateConsumerValidator(ctx, consumerId, val) if err != nil { - // this should never happen but is recoverable if we exclude this validator from the next validator set - k.Logger(ctx).Error("could not create consumer validator", - "validator", val.GetOperator(), - "error", err) - continue + return nextValidators, err } - nextValidators = append(nextValidators, nextValidator) } } - return nextValidators + return nextValidators, nil +} + +// ComputeNextValidators computes the validators for the upcoming epoch based on the currently `bondedValidators`. +func (k Keeper) ComputeNextValidators( + ctx sdk.Context, + consumerId string, + bondedValidators []stakingtypes.Validator, + powerShapingParameters types.PowerShapingParameters, + minPowerToOptIn int64, +) ([]types.ConsensusValidator, error) { + // sort the bonded validators by number of staked tokens in descending order + sort.Slice(bondedValidators, func(i, j int) bool { + return bondedValidators[i].GetBondedTokens().GT(bondedValidators[j].GetBondedTokens()) + }) + + // if inactive validators are not allowed, only consider the first `MaxProviderConsensusValidators` validators + // since those are the ones that participate in consensus + if !powerShapingParameters.AllowInactiveVals { + // only leave the first MaxProviderConsensusValidators bonded validators + maxProviderConsensusVals := k.GetMaxProviderConsensusValidators(ctx) + if len(bondedValidators) > int(maxProviderConsensusVals) { + bondedValidators = bondedValidators[:maxProviderConsensusVals] + } + } + + nextValidators, err := k.FilterValidators(ctx, consumerId, bondedValidators, + func(providerAddr types.ProviderConsAddress) (bool, error) { + canValidateChain, err := k.CanValidateChain(ctx, consumerId, providerAddr, powerShapingParameters.Top_N, minPowerToOptIn) + if err != nil { + return false, err + } + fulfillsMinStake, err := k.FulfillsMinStake(ctx, powerShapingParameters.MinStake, providerAddr) + if err != nil { + return false, err + } + return canValidateChain && fulfillsMinStake, nil + }) + if err != nil { + return []types.ConsensusValidator{}, err + } + + nextValidators = k.CapValidatorSet(ctx, powerShapingParameters, nextValidators) + + nextValidators = k.CapValidatorsPower(ctx, powerShapingParameters.ValidatorsPowerCap, nextValidators) + + return nextValidators, nil } // GetLastBondedValidators iterates the last validator powers in the staking module @@ -200,12 +246,12 @@ func (k Keeper) GetLastBondedValidators(ctx sdk.Context) ([]stakingtypes.Validat if err != nil { return nil, err } - return ccv.GetLastBondedValidatorsUtil(ctx, k.stakingKeeper, k.Logger(ctx), maxVals) + return ccv.GetLastBondedValidatorsUtil(ctx, k.stakingKeeper, maxVals) } // GetLastProviderConsensusActiveValidators returns the `MaxProviderConsensusValidators` many validators with the largest powers // from the last bonded validators in the staking module. func (k Keeper) GetLastProviderConsensusActiveValidators(ctx sdk.Context) ([]stakingtypes.Validator, error) { maxVals := k.GetMaxProviderConsensusValidators(ctx) - return ccv.GetLastBondedValidatorsUtil(ctx, k.stakingKeeper, k.Logger(ctx), uint32(maxVals)) + return ccv.GetLastBondedValidatorsUtil(ctx, k.stakingKeeper, uint32(maxVals)) } diff --git a/x/ccv/provider/keeper/validator_set_update_test.go b/x/ccv/provider/keeper/validator_set_update_test.go index 1b939d08b5..9d8181ff62 100644 --- a/x/ccv/provider/keeper/validator_set_update_test.go +++ b/x/ccv/provider/keeper/validator_set_update_test.go @@ -346,8 +346,10 @@ func TestFilterValidatorsConsiderAll(t *testing.T) { consumerID := CONSUMER_ID // no consumer validators returned if we have no bonded validators - considerAll := func(providerAddr types.ProviderConsAddress) bool { return true } - require.Empty(t, providerKeeper.FilterValidators(ctx, consumerID, []stakingtypes.Validator{}, considerAll)) + considerAll := func(providerAddr types.ProviderConsAddress) (bool, error) { return true, nil } + validators, err := providerKeeper.FilterValidators(ctx, consumerID, []stakingtypes.Validator{}, considerAll) + require.NoError(t, err) + require.Empty(t, validators) var expectedValidators []types.ConsensusValidator @@ -375,7 +377,8 @@ func TestFilterValidatorsConsiderAll(t *testing.T) { }) bondedValidators := []stakingtypes.Validator{valA, valB} - actualValidators := providerKeeper.FilterValidators(ctx, consumerID, bondedValidators, considerAll) + actualValidators, err := providerKeeper.FilterValidators(ctx, consumerID, bondedValidators, considerAll) + require.NoError(t, err) require.Equal(t, expectedValidators, actualValidators) } @@ -386,10 +389,13 @@ func TestFilterValidatorsConsiderOnlyOptIn(t *testing.T) { consumerID := CONSUMER_ID // no consumer validators returned if we have no opted-in validators - require.Empty(t, providerKeeper.FilterValidators(ctx, consumerID, []stakingtypes.Validator{}, - func(providerAddr types.ProviderConsAddress) bool { - return providerKeeper.IsOptedIn(ctx, consumerID, providerAddr) - })) + validators, err := providerKeeper.FilterValidators(ctx, consumerID, []stakingtypes.Validator{}, + func(providerAddr types.ProviderConsAddress) (bool, error) { + return providerKeeper.IsOptedIn(ctx, consumerID, providerAddr), nil + + }) + require.NoError(t, err) + require.Empty(t, validators) var expectedValidators []types.ConsensusValidator @@ -424,10 +430,11 @@ func TestFilterValidatorsConsiderOnlyOptIn(t *testing.T) { // the expected actual validators are the opted-in validators but with the correct power and consumer public keys set bondedValidators := []stakingtypes.Validator{valA, valB} - actualValidators := providerKeeper.FilterValidators(ctx, consumerID, bondedValidators, - func(providerAddr types.ProviderConsAddress) bool { - return providerKeeper.IsOptedIn(ctx, consumerID, providerAddr) + actualValidators, err := providerKeeper.FilterValidators(ctx, consumerID, bondedValidators, + func(providerAddr types.ProviderConsAddress) (bool, error) { + return providerKeeper.IsOptedIn(ctx, consumerID, providerAddr), nil }) + require.NoError(t, err) // sort validators first to be able to compare sortValidators := func(validators []types.ConsensusValidator) { @@ -443,10 +450,11 @@ func TestFilterValidatorsConsiderOnlyOptIn(t *testing.T) { // create a staking validator C that is not opted in, hence `expectedValidators` remains the same valC := createStakingValidator(ctx, mocks, 3, 3) bondedValidators = []stakingtypes.Validator{valA, valB, valC} - actualValidators = providerKeeper.FilterValidators(ctx, consumerID, bondedValidators, - func(providerAddr types.ProviderConsAddress) bool { - return providerKeeper.IsOptedIn(ctx, consumerID, providerAddr) + actualValidators, err = providerKeeper.FilterValidators(ctx, consumerID, bondedValidators, + func(providerAddr types.ProviderConsAddress) (bool, error) { + return providerKeeper.IsOptedIn(ctx, consumerID, providerAddr), nil }) + require.NoError(t, err) sortValidators(actualValidators) sortValidators(expectedValidators) diff --git a/x/ccv/provider/migrations/v8/migrations.go b/x/ccv/provider/migrations/v8/migrations.go index 6afd1888c6..8bf3c4ec18 100644 --- a/x/ccv/provider/migrations/v8/migrations.go +++ b/x/ccv/provider/migrations/v8/migrations.go @@ -305,15 +305,6 @@ func MigrateLaunchedConsumerChains(ctx sdk.Context, store storetypes.KVStore, pk // set phase to launched pk.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) - // This is to migrate everything under `ProviderConsAddrToOptedInConsumerIdsKey` - // `OptedIn` was already re-keyed earlier (see above) and hence we can use `consumerId` here. - for _, providerConsAddr := range pk.GetAllOptedIn(ctx, consumerId) { - err := pk.AppendOptedInConsumerId(ctx, providerConsAddr, consumerId) - if err != nil { - return err - } - } - // set clientId -> consumerId mapping // consumer to client was already re-keyed so we can use `consumerId` here // however, during the rekeying, the reverse index was not set diff --git a/x/ccv/provider/types/keys.go b/x/ccv/provider/types/keys.go index 62869dcebf..368ecf47e6 100644 --- a/x/ccv/provider/types/keys.go +++ b/x/ccv/provider/types/keys.go @@ -145,8 +145,6 @@ const ( RemovalTimeToConsumerIdsKeyName = "RemovalTimeToConsumerIdsKeyName" - ProviderConsAddrToOptedInConsumerIdsKeyName = "ProviderConsAddrToOptedInConsumerIdsKeyName" - ClientIdToConsumerIdKeyName = "ClientIdToConsumerIdKey" ) @@ -375,12 +373,8 @@ func getKeyPrefixes() map[string]byte { // For a specific removal time, it might store multiple consumer chain ids for chains that are to be removed. RemovalTimeToConsumerIdsKeyName: 52, - // ProviderConsAddrToOptedInConsumerIdsKeyName is the key for storing all the consumer ids that a validator - // is currently opted in to. - ProviderConsAddrToOptedInConsumerIdsKeyName: 53, - // ClientIdToConsumerIdKeyName is the key for storing the consumer id for the given client id - ClientIdToConsumerIdKeyName: 54, + ClientIdToConsumerIdKeyName: 53, // NOTE: DO NOT ADD NEW BYTE PREFIXES HERE WITHOUT ADDING THEM TO TestPreserveBytePrefix() IN keys_test.go } @@ -742,12 +736,6 @@ func ParseTime(prefix byte, bz []byte) (time.Time, error) { return timestamp, nil } -// ProviderConsAddrToOptedInConsumerIdsKey returns the key for storing all the consumer ids that `providerAddr` -// has opted-in to -func ProviderConsAddrToOptedInConsumerIdsKey(providerAddr ProviderConsAddress) []byte { - return append([]byte{mustGetKeyPrefix(ProviderConsAddrToOptedInConsumerIdsKeyName)}, providerAddr.ToSdkConsAddr().Bytes()...) -} - // ClientIdToConsumerIdKey returns the consumer id that corresponds to this client id func ClientIdToConsumerIdKey(clientId string) []byte { clientIdLength := len(clientId) diff --git a/x/ccv/provider/types/keys_test.go b/x/ccv/provider/types/keys_test.go index 1fc000e081..5046f231e8 100644 --- a/x/ccv/provider/types/keys_test.go +++ b/x/ccv/provider/types/keys_test.go @@ -140,9 +140,7 @@ func TestPreserveBytePrefix(t *testing.T) { i++ require.Equal(t, byte(52), providertypes.RemovalTimeToConsumerIdsKeyPrefix()) i++ - require.Equal(t, byte(53), providertypes.ProviderConsAddrToOptedInConsumerIdsKey(providertypes.NewProviderConsAddress([]byte{0x05}))[0]) - i++ - require.Equal(t, byte(54), providertypes.ClientIdToConsumerIdKey("clientId")[0]) + require.Equal(t, byte(53), providertypes.ClientIdToConsumerIdKey("clientId")[0]) i++ prefixes := providertypes.GetAllKeyPrefixes() @@ -211,7 +209,6 @@ func getAllFullyDefinedKeys() [][]byte { providertypes.ConsumerIdToRemovalTimeKey("13"), providertypes.SpawnTimeToConsumerIdsKey(time.Time{}), providertypes.RemovalTimeToConsumerIdsKey(time.Time{}), - providertypes.ProviderConsAddrToOptedInConsumerIdsKey(providertypes.NewProviderConsAddress([]byte{0x05})), providertypes.ClientIdToConsumerIdKey("clientId"), } } diff --git a/x/ccv/types/utils.go b/x/ccv/types/utils.go index 15479f9c42..0e6c534606 100644 --- a/x/ccv/types/utils.go +++ b/x/ccv/types/utils.go @@ -12,7 +12,6 @@ import ( host "github.com/cosmos/ibc-go/v8/modules/core/24-host" errorsmod "cosmossdk.io/errors" - "cosmossdk.io/log" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -128,7 +127,7 @@ func GetConsAddrFromBech32(bech32str string) (sdk.ConsAddress, error) { // GetLastBondedValidatorsUtil iterates the last validator powers in the staking module // and returns the first maxVals many validators with the largest powers. -func GetLastBondedValidatorsUtil(ctx sdk.Context, stakingKeeper StakingKeeper, logger log.Logger, maxVals uint32) ([]stakingtypes.Validator, error) { +func GetLastBondedValidatorsUtil(ctx sdk.Context, stakingKeeper StakingKeeper, maxVals uint32) ([]stakingtypes.Validator, error) { // get the bonded validators from the staking module, sorted by power bondedValidators, err := stakingKeeper.GetBondedValidatorsByPower(ctx) if err != nil {