Skip to content

Commit

Permalink
feat!: PSS enable per-consumer chain commission (#1657)
Browse files Browse the repository at this point in the history
* add draft commission

* implement consumer commission draft

* formatting

* add msg handling

* improve UT

* nits

* Update x/ccv/provider/keeper/keeper.go

Co-authored-by: insumity <karolos@informal.systems>

* Update proto/interchain_security/ccv/provider/v1/tx.proto

Co-authored-by: Marius Poke <marius.poke@posteo.de>

* optimize keys

* Update x/ccv/provider/keeper/keeper.go

Co-authored-by: insumity <karolos@informal.systems>

* address comments

* address comments

* remove unnecessary check

* Revert "remove unnecessary check"

This reverts commit 2951e9b.

* fix minor bug in StopConsumerChain

---------

Co-authored-by: insumity <karolos@informal.systems>
Co-authored-by: Marius Poke <marius.poke@posteo.de>
3 people committed Mar 12, 2024
1 parent 71315ef commit e8cd100
Showing 19 changed files with 864 additions and 227 deletions.
2 changes: 1 addition & 1 deletion proto/interchain_security/ccv/provider/v1/provider.proto
Original file line number Diff line number Diff line change
@@ -338,4 +338,4 @@ message OptedInValidator {
int64 power = 3;
// public key used by the validator on the consumer
bytes public_key = 4;
}
}
23 changes: 22 additions & 1 deletion proto/interchain_security/ccv/provider/v1/tx.proto
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ service Msg {
rpc SubmitConsumerDoubleVoting(MsgSubmitConsumerDoubleVoting) returns (MsgSubmitConsumerDoubleVotingResponse);
rpc OptIn(MsgOptIn) returns (MsgOptInResponse);
rpc OptOut(MsgOptOut) returns (MsgOptOutResponse);
rpc SetConsumerCommissionRate(MsgSetConsumerCommissionRate) returns (MsgSetConsumerCommissionRateResponse);
}

message MsgAssignConsumerKey {
@@ -88,4 +89,24 @@ message MsgOptOut {
string provider_addr = 2 [ (gogoproto.moretags) = "yaml:\"address\"" ];
}

message MsgOptOutResponse {}
message MsgOptOutResponse {}

// MsgSetConsumerCommissionRate allows validators to set
// a per-consumer chain commission rate
message MsgSetConsumerCommissionRate {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
// The validator address on the provider
string provider_addr = 1 [ (gogoproto.moretags) = "yaml:\"address\"" ];
// The chain id of the consumer chain to set a commission rate
string chain_id = 2;
// The rate to charge delegators on the consumer chain, as a fraction
string rate = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}


message MsgSetConsumerCommissionRateResponse {}
121 changes: 87 additions & 34 deletions tests/integration/distribution.go
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import (
"cosmossdk.io/math"
abci "github.com/cometbft/cometbft/abci/types"
"github.com/cometbft/cometbft/libs/bytes"
"github.com/cosmos/ibc-go/v7/modules/apps/transfer/types"
distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types"
clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types"
channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types"
@@ -575,7 +575,7 @@ func (s *CCVTestSuite) TestIBCTransferMiddleware() {
bankKeeper := s.providerApp.GetTestBankKeeper()
amount := sdk.NewInt(100)

data = types.NewFungibleTokenPacketData( // can be explicitly changed in setup
data = transfertypes.NewFungibleTokenPacketData( // can be explicitly changed in setup
sdk.DefaultBondDenom,
amount.String(),
authtypes.NewModuleAddress(consumertypes.ConsumerToSendToProviderName).String(),
@@ -734,11 +734,11 @@ func (s *CCVTestSuite) TestAllocateTokens() {
perValExpReward := validatorsExpRewards.QuoDec(sdk.NewDec(int64(valNum)))

// verify the validator tokens allocation
// note all validators have the same voting power to keep things simple
// note that the validators have the same voting power to keep things simple
for _, val := range s.providerChain.Vals.Validators {
valReward := distributionKeeper.GetValidatorOutstandingRewards(s.providerCtx(), sdk.ValAddress(val.Address))
valRewards := distributionKeeper.GetValidatorOutstandingRewards(s.providerCtx(), sdk.ValAddress(val.Address))
s.Require().Equal(
valReward.Rewards,
valRewards.Rewards,
lastValOutRewards[sdk.ValAddress(val.Address).String()].Add(perValExpReward...),
)
}
@@ -902,80 +902,133 @@ func (s *CCVTestSuite) prepareRewardDist() {
}

func (s *CCVTestSuite) TestAllocateTokensToValidator() {

providerkeepr := s.providerApp.GetProviderKeeper()
providerKeeper := s.providerApp.GetProviderKeeper()
distributionKeeper := s.providerApp.GetTestDistributionKeeper()
bankKeeper := s.providerApp.GetTestBankKeeper()

chainID := "consumer"

validators := []bytes.HexBytes{
s.providerChain.Vals.Validators[0].Address,
s.providerChain.Vals.Validators[1].Address,
}

votes := []abci.VoteInfo{
{Validator: abci.Validator{Address: validators[0], Power: 1}},
{Validator: abci.Validator{Address: validators[1], Power: 1}},
}

testCases := []struct {
name string
votes []abci.VoteInfo
tokens sdk.DecCoins
expCoinTransferred sdk.DecCoins
name string
votes []abci.VoteInfo
tokens sdk.DecCoins
rate sdk.Dec
expAllocated sdk.DecCoins
}{
{
name: "reward tokens are empty",
name: "tokens are empty",
tokens: sdk.DecCoins{},
rate: sdk.ZeroDec(),
expAllocated: nil,
},
{
name: "total voting power is zero",
tokens: sdk.DecCoins{sdk.NewDecCoin("uatom", math.NewInt(100_000))},
name: "total voting power is zero",
tokens: sdk.DecCoins{sdk.NewDecCoin(sdk.DefaultBondDenom, math.NewInt(100_000))},
rate: sdk.ZeroDec(),
expAllocated: nil,
},
{
name: "expect all tokens to be allocated to a single validator",
votes: []abci.VoteInfo{votes[0]},
tokens: sdk.DecCoins{sdk.NewDecCoin("uatom", math.NewInt(100_000))},
expCoinTransferred: sdk.DecCoins{sdk.NewDecCoin("uatom", math.NewInt(100_000))},
name: "expect all tokens to be allocated to a single validator",
votes: []abci.VoteInfo{votes[0]},
tokens: sdk.DecCoins{sdk.NewDecCoin(sdk.DefaultBondDenom, math.NewInt(999))},
rate: sdk.NewDecWithPrec(5, 1),
expAllocated: sdk.DecCoins{sdk.NewDecCoin(sdk.DefaultBondDenom, math.NewInt(999))},
},
{
name: "expect tokens to be allocated evenly between validators",
votes: []abci.VoteInfo{votes[0], votes[1]},
tokens: sdk.DecCoins{sdk.NewDecCoin("uatom", math.NewInt(555_555))},
expCoinTransferred: sdk.DecCoins{sdk.NewDecCoin("uatom", math.NewInt(555_555))},
name: "expect tokens to be allocated evenly between validators",
votes: []abci.VoteInfo{votes[0], votes[1]},
tokens: sdk.DecCoins{sdk.NewDecCoinFromDec(sdk.DefaultBondDenom, math.LegacyNewDecFromIntWithPrec(math.NewInt(999), 2))},
rate: sdk.OneDec(),
expAllocated: sdk.DecCoins{sdk.NewDecCoinFromDec(sdk.DefaultBondDenom, math.LegacyNewDecFromIntWithPrec(math.NewInt(999), 2))},
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
// TODO: opt validators in and verify
// that rewards are solely allocated to them

// set the same consumer commission rate for all validators
for _, v := range s.providerChain.Vals.Validators {
provAddr := providertypes.NewProviderConsAddress(sdk.ConsAddress(v.Address))

providerKeeper.SetConsumerCommissionRate(
s.providerCtx(),
chainID,
provAddr,
tc.rate,
)
}

// TODO: opt validators in and verify
// that rewards are only allocated to them
ctx, _ := s.providerCtx().CacheContext()

// allocate tokens
res := providerkeepr.AllocateTokensToConsumerValidators(
res := providerKeeper.AllocateTokensToConsumerValidators(
ctx,
chainID,
tc.votes,
tc.tokens,
)

// check that the expect result is returned
s.Require().Equal(tc.expCoinTransferred, res)
// check that the expected result is returned
s.Require().Equal(tc.expAllocated, res)

if !tc.expCoinTransferred.Empty() {
if !tc.expAllocated.Empty() {
// rewards are expected to be allocated evenly between validators
rewardsPerVal := tc.expCoinTransferred.QuoDec(sdk.NewDec(int64(len(tc.votes))))
rewardsPerVal := tc.expAllocated.QuoDec(sdk.NewDec(int64(len(tc.votes))))

// check that the rewards are allocated to validators as expected
// check that the rewards are allocated to validators
for _, v := range tc.votes {
valAddr := sdk.ValAddress(v.Validator.Address)
rewards := s.providerApp.GetTestDistributionKeeper().GetValidatorOutstandingRewards(
ctx,
sdk.ValAddress(v.Validator.Address),
valAddr,
)
s.Require().Equal(rewardsPerVal, rewards.Rewards)

// send rewards to the distribution module
valRewardsTrunc, _ := rewards.Rewards.TruncateDecimal()
err := bankKeeper.SendCoinsFromAccountToModule(
ctx,
s.providerChain.SenderAccount.GetAddress(),
distrtypes.ModuleName,
valRewardsTrunc)
s.Require().NoError(err)

// check that validators can withdraw their rewards
withdrawnCoins, err := distributionKeeper.WithdrawValidatorCommission(
ctx,
valAddr,
)
s.Require().NoError(err)

// check that the withdrawn coins is equal to the entire reward amount
// times the set consumer commission rate
commission := rewards.Rewards.MulDec(tc.rate)
c, _ := commission.TruncateDecimal()
s.Require().Equal(withdrawnCoins, c)

// check that validators get rewards in their balance
s.Require().Equal(withdrawnCoins, bankKeeper.GetAllBalances(ctx, sdk.AccAddress(valAddr)))
}
} else {
for _, v := range tc.votes {
valAddr := sdk.ValAddress(v.Validator.Address)
rewards := s.providerApp.GetTestDistributionKeeper().GetValidatorOutstandingRewards(
ctx,
valAddr,
)
s.Require().Zero(rewards.Rewards)
}
}

})
}
}
1 change: 1 addition & 0 deletions testutil/integration/interfaces.go
Original file line number Diff line number Diff line change
@@ -142,6 +142,7 @@ type TestDistributionKeeper interface {
GetValidatorOutstandingRewards(ctx sdk.Context,
val sdk.ValAddress) (rewards distributiontypes.ValidatorOutstandingRewards)
GetCommunityTax(ctx sdk.Context) (percent sdk.Dec)
WithdrawValidatorCommission(ctx sdk.Context, valAddr sdk.ValAddress) (sdk.Coins, error)
}

type TestMintKeeper interface {
128 changes: 0 additions & 128 deletions testutil/keeper/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions testutil/keeper/unit_test_helpers.go
Original file line number Diff line number Diff line change
@@ -249,10 +249,15 @@ func TestProviderStateIsCleanedAfterConsumerChainIsStopped(t *testing.T, ctx sdk

require.Empty(t, providerKeeper.GetAllVscSendTimestamps(ctx, expectedChainID))

// in case the chain was successfully stopped, it should not contain a Top N associated to it
_, found = providerKeeper.GetTopN(ctx, expectedChainID)
require.False(t, found)

// test key assignment state is cleaned
require.Empty(t, providerKeeper.GetAllValidatorConsumerPubKeys(ctx, &expectedChainID))
require.Empty(t, providerKeeper.GetAllValidatorsByConsumerAddr(ctx, &expectedChainID))
require.Empty(t, providerKeeper.GetAllConsumerAddrsToPrune(ctx, expectedChainID))
require.Empty(t, providerKeeper.GetAllCommissionRateValidators(ctx, expectedChainID))
}

func GetTestConsumerAdditionProp() *providertypes.ConsumerAdditionProposal {
53 changes: 43 additions & 10 deletions x/ccv/provider/keeper/distribution.go
Original file line number Diff line number Diff line change
@@ -5,9 +5,9 @@ import (
"cosmossdk.io/math"
abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types"

distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types"
"github.com/cosmos/interchain-security/v4/x/ccv/provider/types"
)

@@ -138,16 +138,17 @@ func (k Keeper) AllocateTokensToConsumerValidators(
chainID string,
bondedVotes []abci.VoteInfo,
tokens sdk.DecCoins,
) (totalReward sdk.DecCoins) {
) (allocated sdk.DecCoins) {

// return early if the tokens are empty
if tokens.Empty() {
return totalReward
return allocated
}

// get the consumer total voting power from the votes
totalPower := k.ComputeConsumerTotalVotingPower(ctx, chainID, bondedVotes)
if totalPower == 0 {
return totalReward
return allocated
}

for _, vote := range bondedVotes {
@@ -157,19 +158,31 @@ func (k Keeper) AllocateTokensToConsumerValidators(
powerFraction := math.LegacyNewDec(vote.Validator.Power).QuoTruncate(math.LegacyNewDec(totalPower))
tokensFraction := tokens.MulDecTruncate(powerFraction)

// get the validator type struct for the consensus address
val := k.stakingKeeper.ValidatorByConsAddr(ctx, consAddr).(stakingtypes.Validator)

// check if the validator set a custom commission rate for the consumer chain
if cr, found := k.GetConsumerCommissionRate(ctx, chainID, types.NewProviderConsAddress(consAddr)); found {
// set the validator commission rate
val.Commission.CommissionRates.Rate = cr
}

// allocate the consumer reward tokens to the validator
k.distributionKeeper.AllocateTokensToValidator(
ctx,
k.stakingKeeper.ValidatorByConsAddr(ctx, consAddr),
val,
tokensFraction,
)
totalReward = totalReward.Add(tokensFraction...)

// sum the tokens allocated
allocated = allocated.Add(tokensFraction...)
}

return totalReward
return allocated
}

// TransferConsumerRewardsToDistributionModule transfers the collected rewards of the given consumer chain
// from the consumer rewards pool module account to a the distribution module
// TransferConsumerRewardsToDistributionModule transfers the rewards allocation of the given consumer chain
// from the consumer rewards pool to a the distribution module
func (k Keeper) TransferConsumerRewardsToDistributionModule(
ctx sdk.Context,
chainID string,
@@ -267,3 +280,23 @@ func (k Keeper) IdentifyConsumerChainIDFromIBCPacket(ctx sdk.Context, packet cha

return chainID, nil
}

// HandleSetConsumerCommissionRate sets a per-consumer chain commission rate for the given provider address
// on the condition that the given consumer chain exists.
func (k Keeper) HandleSetConsumerCommissionRate(ctx sdk.Context, chainID string, providerAddr types.ProviderConsAddress, commissionRate sdk.Dec) error {
// check that the consumer chain exists
if !k.IsConsumerProposedOrRegistered(ctx, chainID) {
return errorsmod.Wrapf(
types.ErrUnknownConsumerChainId,
"unknown consumer chain, with id: %s", chainID)
}
// set per-consumer chain commission rate for the validator address
k.SetConsumerCommissionRate(
ctx,
chainID,
providerAddr,
commissionRate,
)

return nil
}
69 changes: 69 additions & 0 deletions x/ccv/provider/keeper/keeper.go
Original file line number Diff line number Diff line change
@@ -1248,6 +1248,75 @@ func (k Keeper) SetToBeOptedIn(
store.Set(types.ToBeOptedInKey(chainID, providerAddr), []byte{})
}

// SetConsumerCommissionRate sets a per-consumer chain commission rate
// for the given validator address
func (k Keeper) SetConsumerCommissionRate(
ctx sdk.Context,
chainID string,
providerAddr types.ProviderConsAddress,
commissionRate sdk.Dec,
) {
store := ctx.KVStore(k.storeKey)
bz, err := commissionRate.Marshal()
if err != nil {
panic(fmt.Errorf("consumer commission rate marshalling failed: %s", err))
}

store.Set(types.ConsumerCommissionRateKey(chainID, providerAddr), bz)
}

// GetConsumerCommissionRate returns the per-consumer commission rate set
// for the given validator address
func (k Keeper) GetConsumerCommissionRate(
ctx sdk.Context,
chainID string,
providerAddr types.ProviderConsAddress,
) (sdk.Dec, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.ConsumerCommissionRateKey(chainID, providerAddr))
if bz == nil {
return sdk.ZeroDec(), false
}

cr := sdk.Dec{}
if err := cr.Unmarshal(bz); err != nil {
k.Logger(ctx).Error("consumer commission rate unmarshalling failed: %s", err)
return sdk.ZeroDec(), false
}

return cr, true
}

// GetAllCommissionRateValidators returns all the validator address
// that set a commission rate for the given chain ID
func (k Keeper) GetAllCommissionRateValidators(
ctx sdk.Context,
chainID string) (addresses []types.ProviderConsAddress) {

store := ctx.KVStore(k.storeKey)
key := types.ChainIdWithLenKey(types.ConsumerCommissionRatePrefix, chainID)
iterator := sdk.KVStorePrefixIterator(store, key)
defer iterator.Close()

for ; iterator.Valid(); iterator.Next() {
providerAddr := types.NewProviderConsAddress(iterator.Key()[len(key):])
addresses = append(addresses, providerAddr)
}

return addresses
}

// DeleteConsumerCommissionRate the per-consumer chain commission rate
// associated to the given validator address
func (k Keeper) DeleteConsumerCommissionRate(
ctx sdk.Context,
chainID string,
providerAddr types.ProviderConsAddress,
) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.ConsumerCommissionRateKey(chainID, providerAddr))
}

func (k Keeper) DeleteToBeOptedIn(
ctx sdk.Context,
chainID string,
41 changes: 40 additions & 1 deletion x/ccv/provider/keeper/keeper_test.go
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ import (
abci "github.com/cometbft/cometbft/abci/types"
tmprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto"

sdk "github.com/cosmos/cosmos-sdk/types"
cryptotestutil "github.com/cosmos/interchain-security/v4/testutil/crypto"
testkeeper "github.com/cosmos/interchain-security/v4/testutil/keeper"
"github.com/cosmos/interchain-security/v4/x/ccv/provider/types"
@@ -692,7 +693,8 @@ func TestGetAllOptedIn(t *testing.T) {
ProviderAddr: expectedOptedInValidator.ProviderAddr,
BlockHeight: expectedOptedInValidator.BlockHeight,
Power: expectedOptedInValidator.Power,
PublicKey: expectedOptedInValidator.PublicKey})
PublicKey: expectedOptedInValidator.PublicKey,
})
}

actualOptedInValidators := providerKeeper.GetAllOptedIn(ctx, "chainID")
@@ -858,3 +860,40 @@ func TestToBeOptedOut(t *testing.T) {
providerKeeper.DeleteToBeOptedOut(ctx, "chainID", providerAddr)
require.False(t, providerKeeper.IsToBeOptedOut(ctx, "chainID", providerAddr))
}

// TestToBeOptedOut tests the `SetConsumerCommissionRate`, `GetConsumerCommissionRate`, and `DeleteConsumerCommissionRate` methods
func TestConsumerCommissionRate(t *testing.T) {
providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t))
defer ctrl.Finish()

providerAddr1 := types.NewProviderConsAddress([]byte("providerAddr1"))
providerAddr2 := types.NewProviderConsAddress([]byte("providerAddr2"))

cr, found := providerKeeper.GetConsumerCommissionRate(ctx, "chainID", providerAddr1)
require.False(t, found)
require.Equal(t, sdk.ZeroDec(), cr)

providerKeeper.SetConsumerCommissionRate(ctx, "chainID", providerAddr1, sdk.OneDec())
cr, found = providerKeeper.GetConsumerCommissionRate(ctx, "chainID", providerAddr1)
require.True(t, found)
require.Equal(t, sdk.OneDec(), cr)

providerKeeper.SetConsumerCommissionRate(ctx, "chainID", providerAddr2, sdk.ZeroDec())
cr, found = providerKeeper.GetConsumerCommissionRate(ctx, "chainID", providerAddr2)
require.True(t, found)
require.Equal(t, sdk.ZeroDec(), cr)

provAddrs := providerKeeper.GetAllCommissionRateValidators(ctx, "chainID")
require.Len(t, provAddrs, 2)

for _, addr := range provAddrs {
providerKeeper.DeleteConsumerCommissionRate(ctx, "chainID", addr)
}

_, found = providerKeeper.GetConsumerCommissionRate(ctx, "chainID", providerAddr1)
require.False(t, found)

_, found = providerKeeper.GetConsumerCommissionRate(ctx, "chainID", providerAddr2)
require.False(t, found)

}
41 changes: 37 additions & 4 deletions x/ccv/provider/keeper/msg_server.go
Original file line number Diff line number Diff line change
@@ -2,14 +2,12 @@ package keeper

import (
"context"
errorsmod "cosmossdk.io/errors"

errorsmod "cosmossdk.io/errors"
tmtypes "github.com/cometbft/cometbft/types"
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

tmtypes "github.com/cometbft/cometbft/types"

"github.com/cosmos/interchain-security/v4/x/ccv/provider/types"
ccvtypes "github.com/cosmos/interchain-security/v4/x/ccv/types"
)
@@ -195,3 +193,38 @@ func (k msgServer) OptOut(goCtx context.Context, msg *types.MsgOptOut) (*types.M

return &types.MsgOptOutResponse{}, nil
}

func (k msgServer) SetConsumerCommissionRate(goCtx context.Context, msg *types.MsgSetConsumerCommissionRate) (*types.MsgSetConsumerCommissionRateResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

providerValidatorAddr, err := sdk.ValAddressFromBech32(msg.ProviderAddr)
if err != nil {
return nil, err
}

// validator must already be registered
validator, found := k.stakingKeeper.GetValidator(ctx, providerValidatorAddr)
if !found {
return nil, stakingtypes.ErrNoValidatorFound
}

consAddr, err := validator.GetConsAddr()
if err != nil {
return nil, err
}

if err := k.HandleSetConsumerCommissionRate(ctx, msg.ChainId, types.NewProviderConsAddress(consAddr), msg.Rate); err != nil {
return nil, err
}

ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventTypeSetConsumerCommissionRate,
sdk.NewAttribute(types.AttributeConsumerChainID, msg.ChainId),
sdk.NewAttribute(types.AttributeProviderValidatorAddress, msg.ProviderAddr),
sdk.NewAttribute(types.AttributeConsumerCommissionRate, msg.Rate.String()),
),
})

return &types.MsgSetConsumerCommissionRateResponse{}, nil
}
25 changes: 25 additions & 0 deletions x/ccv/provider/keeper/partial_set_security_test.go
Original file line number Diff line number Diff line change
@@ -112,3 +112,28 @@ func TestHandleOptOut(t *testing.T) {
require.NoError(t, err)
require.False(t, providerKeeper.IsToBeOptedOut(ctx, "chainID", providerAddr))
}

func TestHandleSetConsumerCommissionRate(t *testing.T) {
providerKeeper, ctx, ctrl, _ := 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, sdk.ZeroDec()))

// setup a pending consumer chain
chainID := "pendingChainID"
providerKeeper.SetPendingConsumerAdditionProp(ctx, &types.ConsumerAdditionProposal{ChainId: chainID})

// check that there's no commission rate set for the validator yet
_, found := providerKeeper.GetConsumerCommissionRate(ctx, chainID, providerAddr)
require.False(t, found)

require.NoError(t, providerKeeper.HandleSetConsumerCommissionRate(ctx, chainID, providerAddr, sdk.OneDec()))

// check that the commission rate is now set
cr, found := providerKeeper.GetConsumerCommissionRate(ctx, chainID, providerAddr)
require.Equal(t, sdk.OneDec(), cr)
require.True(t, found)
}
6 changes: 6 additions & 0 deletions x/ccv/provider/keeper/proposal.go
Original file line number Diff line number Diff line change
@@ -190,6 +190,12 @@ func (k Keeper) StopConsumerChain(ctx sdk.Context, chainID string, closeChan boo
k.DeleteVscSendTimestampsForConsumer(ctx, chainID)
}

// delete consumer commission rate
provAddrs := k.GetAllCommissionRateValidators(ctx, chainID)
for _, addr := range provAddrs {
k.DeleteConsumerCommissionRate(ctx, chainID, addr)
}

k.DeleteInitChainHeight(ctx, chainID)
k.DeleteSlashAcks(ctx, chainID)
k.DeletePendingVSCPackets(ctx, chainID)
4 changes: 0 additions & 4 deletions x/ccv/provider/keeper/proposal_test.go
Original file line number Diff line number Diff line change
@@ -554,10 +554,6 @@ func TestStopConsumerChain(t *testing.T) {
require.Error(t, err)
} else {
require.NoError(t, err)

// in case the chain was successfully stopped, it should not contain a Top N associated to it
_, found := providerKeeper.GetTopN(ctx, "chainID")
require.False(t, found)
}

testkeeper.TestProviderStateIsCleanedAfterConsumerChainIsStopped(t, ctx, providerKeeper, "chainID", "channelID")
1 change: 1 addition & 0 deletions x/ccv/provider/types/errors.go
Original file line number Diff line number Diff line change
@@ -24,4 +24,5 @@ var (
ErrInvalidConsumerClient = errorsmod.Register(ModuleName, 16, "ccv channel is not built on correct client")
ErrDuplicateConsumerChain = errorsmod.Register(ModuleName, 17, "consumer chain already exists")
ErrConsumerChainNotFound = errorsmod.Register(ModuleName, 18, "consumer chain not found")
ErrInvalidConsumerCommissionRate = errorsmod.Register(ModuleName, 19, "consumer commission rate is invalid")
)
3 changes: 3 additions & 0 deletions x/ccv/provider/types/events.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ const (
EventTypeAddConsumerRewardDenom = "add_consumer_reward_denom"
EventTypeRemoveConsumerRewardDenom = "remove_consumer_reward_denom"
EventTypeExecuteConsumerChainSlash = "execute_consumer_chain_slash"
EventTypeSetConsumerCommissionRate = "set_consumer_commission_rate"
AttributeInfractionHeight = "infraction_height"
AttributeInitialHeight = "initial_height"
AttributeInitializationTimeout = "initialization_timeout"
@@ -15,4 +16,6 @@ const (
AttributeProviderValidatorAddress = "provider_validator_address"
AttributeConsumerConsensusPubKey = "consumer_consensus_pub_key"
AttributeConsumerRewardDenom = "consumer_reward_denom"
AttributeConsumerCommissionRate = "consumer_commission_rate"
AttributeConsumerChainID = "consumer_chain_id"
)
13 changes: 13 additions & 0 deletions x/ccv/provider/types/keys.go
Original file line number Diff line number Diff line change
@@ -169,6 +169,10 @@ const (
// it allocated to the consumer rewards pool
ConsumerRewardsAllocationBytePrefix

// ConsumerCommissionRatePrefix is the byte prefix used when storing a validator a per-consumer chain commission rate
// for a validator address
ConsumerCommissionRatePrefix

// NOTE: DO NOT ADD NEW BYTE PREFIXES HERE WITHOUT ADDING THEM TO getAllKeyPrefixes() IN keys_test.go
)

@@ -569,6 +573,15 @@ func ConsumerRewardsAllocationKey(chainID string) []byte {
return append([]byte{ConsumerRewardsAllocationBytePrefix}, []byte(chainID)...)
}

// ConsumerCommissionRateKey returns the key of consumer chain `chainID` and validator with `providerAddr`
func ConsumerCommissionRateKey(chainID string, providerAddr ProviderConsAddress) []byte {
return ChainIdAndConsAddrKey(
ConsumerCommissionRatePrefix,
chainID,
providerAddr.ToSdkConsAddr(),
)
}

//
// End of generic helpers section
//
1 change: 1 addition & 0 deletions x/ccv/provider/types/keys_test.go
Original file line number Diff line number Diff line change
@@ -62,6 +62,7 @@ func getAllKeyPrefixes() []byte {
providertypes.ToBeOptedInBytePrefix,
providertypes.ToBeOptedOutBytePrefix,
providertypes.ConsumerRewardsAllocationBytePrefix,
providertypes.ConsumerCommissionRatePrefix,
}
}

44 changes: 44 additions & 0 deletions x/ccv/provider/types/msg.go
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ const (
TypeMsgSubmitConsumerDoubleVoting = "submit_consumer_double_vote"
TypeMsgOptIn = "opt_in"
TypeMsgOptOut = "opt_out"
TypeMsgSetConsumerCommissionRate = "set_consumer_commission_rate"
)

var (
@@ -32,6 +33,7 @@ var (
_ sdk.Msg = &MsgSubmitConsumerDoubleVoting{}
_ sdk.Msg = &MsgOptIn{}
_ sdk.Msg = &MsgOptOut{}
_ sdk.Msg = &MsgSetConsumerCommissionRate{}
)

// NewMsgAssignConsumerKey creates a new MsgAssignConsumerKey instance.
@@ -316,3 +318,45 @@ func (msg MsgOptOut) ValidateBasic() error {
func (msg MsgOptOut) Type() string {
return TypeMsgOptOut
}

func (msg MsgSetConsumerCommissionRate) Route() string {
return RouterKey
}

func (msg MsgSetConsumerCommissionRate) Type() string {
return TypeMsgSetConsumerCommissionRate
}

func (msg MsgSetConsumerCommissionRate) ValidateBasic() error {
if strings.TrimSpace(msg.ChainId) == "" {
return errorsmod.Wrapf(ErrInvalidConsumerChainID, "chainId cannot be blank")
}

if 128 < len(msg.ChainId) {
return errorsmod.Wrapf(ErrInvalidConsumerChainID, "chainId cannot exceed 128 length")
}
_, err := sdk.ValAddressFromBech32(msg.ProviderAddr)
if err != nil {
return ErrInvalidProviderAddress
}

if msg.Rate.IsNegative() || msg.Rate.GT(sdk.OneDec()) {
return errorsmod.Wrapf(ErrInvalidConsumerCommissionRate, "consumer commission rate should be in the range [0, 1]")
}

return nil
}

func (msg MsgSetConsumerCommissionRate) GetSigners() []sdk.AccAddress {
valAddr, err := sdk.ValAddressFromBech32(msg.ProviderAddr)
if err != nil {
// same behavior as in cosmos-sdk
panic(err)
}
return []sdk.AccAddress{valAddr.Bytes()}
}

func (msg MsgSetConsumerCommissionRate) GetSignBytes() []byte {
bz := ccvtypes.ModuleCdc.MustMarshalJSON(&msg)
return sdk.MustSortJSON(bz)
}
510 changes: 466 additions & 44 deletions x/ccv/provider/types/tx.pb.go

Large diffs are not rendered by default.

0 comments on commit e8cd100

Please sign in to comment.