Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: extend consumer_validators query to return consumer valset before launch #2164

Merged
merged 61 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
1edbf8e
(partially) renamed chain ids to consumer ids
insumity Jul 30, 2024
3ba933f
renamed proposal messages
insumity Jul 30, 2024
489cefa
removed global slash entry
insumity Jul 30, 2024
2d2c41e
fixed unit tests
insumity Jul 30, 2024
f2ad00a
added new messages
insumity Jul 30, 2024
c7b5386
introduced new state
insumity Jul 30, 2024
010db3a
added functionality for the register and initialize messages
insumity Jul 31, 2024
ecb89c0
renamed (partially) chainIds to consumerIds
insumity Jul 31, 2024
758c52f
set consumerId to chainId association during registration
insumity Jul 31, 2024
e055f0f
added extra check in the initialization so unknokwn, launched, or sto…
insumity Jul 31, 2024
e692a22
added initial work on traversing initialized chains that are to-be-la…
insumity Jul 31, 2024
fda23de
fixed rebase issues after bringing the VSCMaturedPackets work in
insumity Aug 1, 2024
1146509
made it so we traverse initialization records instead of addition pro…
insumity Aug 2, 2024
2256ab3
renamed more chainIDs to consumerIds
insumity Aug 2, 2024
caa91b7
removed ClientIdToChainId state because chainId already resides on th…
insumity Aug 2, 2024
e969fe3
nit fixes in go docs
insumity Aug 2, 2024
cef3be7
removed MsgConsumerAddition
insumity Aug 2, 2024
2385aca
added CLI commands for new messages
insumity Aug 2, 2024
8a0ba7d
removed consumer modification proposal
insumity Aug 2, 2024
d6f890a
removed (partially) consumer removal proposal
insumity Aug 5, 2024
64ed6e7
rebased to pick up the inactive-validators work (PR #2079)
insumity Aug 6, 2024
1f76f20
introduced consumerId in the equivocation messages (and a useful quer…
insumity Aug 6, 2024
f95b248
added safeguard so that a validator cannot opt-in to two different ch…
insumity Aug 6, 2024
e40449f
renamed some chainIDs to consumerIds
insumity Aug 6, 2024
3d5dbb1
updated based on comments
insumity Aug 13, 2024
8a0b825
fixed integration tests
insumity Aug 14, 2024
96b5706
rebased to pick up the removal of legacy proposals (#2130) and re-int…
insumity Aug 15, 2024
a9b05a3
changes messages to only have MsgCreateConsumer and MsgUpdateConsumer…
insumity Aug 19, 2024
1ac674f
cleaned up slightly a few things (mostly committing & pushing) so peo…
insumity Aug 20, 2024
739bd21
fixed the CreateConsumer and UpdateConsumer logic and made most of th…
insumity Aug 20, 2024
5e5480f
fixed hooks and the code around proposalId to consumerId
insumity Aug 21, 2024
7c43f6d
pre-spawn query
sainoe Aug 21, 2024
73ce1f6
rebase
sainoe Aug 22, 2024
9e4fbde
nits
sainoe Aug 22, 2024
da3cbb2
feat: extend consumer validator query to return commission rate (back…
sainoe Aug 22, 2024
e5cbcd0
remove panics
sainoe Aug 22, 2024
db8b887
renamed some chainIds to consumerIds
insumity Aug 21, 2024
4744bdc
took into account comments and also added safeguard to reject new pro…
insumity Aug 22, 2024
715bbbc
Update x/ccv/provider/types/msg.go
insumity Aug 22, 2024
96029ca
removed double-gas charge on MsgCreateConsumer and imroved the logic …
insumity Aug 23, 2024
8beffdc
added PopulateMinimumPowerInTopN tested
insumity Aug 23, 2024
ec5ccec
took into account comments (using protos for marshalling string slice…
insumity Aug 23, 2024
b733bd7
update logic
sainoe Aug 23, 2024
96ef414
took into account comments (using protos for marshalling string slice…
insumity Aug 23, 2024
7580046
Merge branch 'insumity/rename-chain-id' into sainoe/consu-valset-query
sainoe Aug 23, 2024
563774b
feat: add fields to consumer validators query (#2167)
sainoe Aug 23, 2024
6992a00
order consumer validators
sainoe Aug 26, 2024
6c551f8
nits
sainoe Aug 26, 2024
05fd5f3
feat: first iteration on Permissionless ICS (#2117)
insumity Aug 26, 2024
ad5288d
Merge branch 'feat/permissionless' into sainoe/consu-valset-query
sainoe Aug 26, 2024
fca239b
cover stopped phase case
sainoe Aug 26, 2024
29d790c
fixed bug on removing previous spawn time & added tests
insumity Aug 26, 2024
a400de1
added some additional tests
insumity Aug 26, 2024
84565ab
added tests on the hooks
insumity Aug 27, 2024
0fc6968
removed check that spawn time is in the future
insumity Aug 27, 2024
19be098
feat: refactor consumer validator set computation (#2175)
sainoe Aug 27, 2024
cc4f6b9
Merge branch 'feat/permissionless' into sainoe/consu-valset-query
sainoe Aug 27, 2024
43baef1
nit
sainoe Aug 28, 2024
06bdfc4
nits
sainoe Aug 28, 2024
7b48b97
Merge branch 'feat/permissionless' into sainoe/consu-valset-query
sainoe Aug 28, 2024
9403c26
nit
sainoe Aug 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 46 additions & 7 deletions x/ccv/provider/keeper/grpc_query.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package keeper

import (
"bytes"
"context"
"fmt"
"sort"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand Down Expand Up @@ -333,18 +335,55 @@ func (k Keeper) QueryConsumerValidators(goCtx context.Context, req *types.QueryC

ctx := sdk.UnwrapSDKContext(goCtx)

if _, found := k.GetConsumerClientId(ctx, consumerId); !found {
// chain has to have started; consumer client id is set for a chain during the chain's spawn time
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("no started consumer chain: %s", consumerId))
// get the consumer phase
phase := k.GetConsumerPhase(ctx, consumerId)
if phase == types.ConsumerPhase_CONSUMER_PHASE_UNSPECIFIED {
return nil, status.Errorf(codes.InvalidArgument, "cannot find a phase for consumer: %s", consumerId)
}

var validators []*types.QueryConsumerValidatorsValidator
// query consumer validator set

consumerValSet, err := k.GetConsumerValSet(ctx, consumerId)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
var consumerValSet []types.ConsensusValidator
var err error

// if the consumer launched, the consumer valset has been persisted
if phase == types.ConsumerPhase_CONSUMER_PHASE_LAUNCHED {
consumerValSet, err = k.GetConsumerValSet(ctx, consumerId)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
// if the consumer hasn't been launched or stopped, compute the consumer validator set
} else if phase != types.ConsumerPhase_CONSUMER_PHASE_STOPPED {
bondedValidators, err := k.GetLastBondedValidators(ctx)
if err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to get last validators: %s", err))
}
minPower := int64(0)
// for TopN chains, compute the minPower that will be automatically opted in
if topN := k.GetTopN(ctx, consumerId); topN > 0 {
mpoke marked this conversation as resolved.
Show resolved Hide resolved
sainoe marked this conversation as resolved.
Show resolved Hide resolved
activeValidators, err := k.GetLastProviderConsensusActiveValidators(ctx)
if err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to get active validators: %s", err))
}

minPower, err = k.ComputeMinPowerInTopN(ctx, activeValidators, topN)
if err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to compute min power to opt in for chain %s: %s", consumerId, err))
}
}

consumerValSet = k.ComputeNextValidators(ctx, consumerId, bondedValidators, minPower)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider modularizing the logic for computing the validator set.

The logic for computing the validator set is repeated in several places. Consider modularizing it into a separate function to avoid code duplication.

Consider creating a separate function to compute the validator set and reuse it in different parts of the codebase.


// 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 {
mpoke marked this conversation as resolved.
Show resolved Hide resolved
return bytes.Compare(
consumerValSet[i].ProviderConsAddr,
consumerValSet[j].ProviderConsAddr,
) == -1
})
}

var validators []*types.QueryConsumerValidatorsValidator
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider modularizing the logic for creating validator response objects.

The logic for creating validator response objects is repeated in several places. Consider modularizing it into a separate function to avoid code duplication.

Consider creating a separate function to create validator response objects and reuse it in different parts of the codebase.

for _, consumerVal := range consumerValSet {
provAddr := types.ProviderConsAddress{Address: consumerVal.ProviderConsAddr}
consAddr := provAddr.ToSdkConsAddr()
Expand Down
135 changes: 119 additions & 16 deletions x/ccv/provider/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package keeper_test

import (
"bytes"
"fmt"
"sort"
"testing"

"github.com/golang/mock/gomock"
Expand Down Expand Up @@ -101,41 +103,84 @@ func TestQueryConsumerValidators(t *testing.T) {
defer ctrl.Finish()

consumerId := "0"

req := types.QueryConsumerValidatorsRequest{
ConsumerId: consumerId,
}

// error returned from not-started chain
// error returned from not-existing chain
_, err := pk.QueryConsumerValidators(ctx, &req)
require.Error(t, err)

// set the consumer to the "registered" phase
pk.SetConsumerPhase(ctx, consumerId, types.ConsumerPhase_CONSUMER_PHASE_REGISTERED)

// expect empty valset
testkeeper.SetupMocksForLastBondedValidatorsExpectation(mocks.MockStakingKeeper, 0, []stakingtypes.Validator{}, 1) // -1 to allow the calls "AnyTimes"
res, err := pk.QueryConsumerValidators(ctx, &req)
require.NoError(t, err)
require.Len(t, res.Validators, 0)

// create bonded validators
val1 := createStakingValidator(ctx, mocks, 1, 1, 1)
pk1, _ := val1.CmtConsPublicKey()
valConsAddr1, _ := val1.GetConsAddr()
providerAddr1 := types.NewProviderConsAddress(valConsAddr1)
pk1, _ := val1.CmtConsPublicKey()
consumerValidator1 := types.ConsensusValidator{ProviderConsAddr: providerAddr1.ToSdkConsAddr(), Power: 1, PublicKey: &pk1}
val1.Tokens = sdk.TokensFromConsensusPower(1, sdk.DefaultPowerReduction)
val1.Description = stakingtypes.Description{Moniker: "ConsumerValidator1"}
val1.Commission.Rate = math.LegacyMustNewDecFromStr("0.123")

val2 := createStakingValidator(ctx, mocks, 1, 2, 2)
pk2, _ := val2.CmtConsPublicKey()
valConsAddr2, _ := val2.GetConsAddr()
providerAddr2 := types.NewProviderConsAddress(valConsAddr2)
pk2, _ := val2.CmtConsPublicKey()
consumerValidator2 := types.ConsensusValidator{ProviderConsAddr: providerAddr2.ToSdkConsAddr(), Power: 2, PublicKey: &pk2}
val2.Tokens = sdk.TokensFromConsensusPower(2, sdk.DefaultPowerReduction)
val2.Description = stakingtypes.Description{Moniker: "ConsumerValidator2"}
val2.Commission.Rate = math.LegacyMustNewDecFromStr("0.123")
val2.Commission.Rate = math.LegacyMustNewDecFromStr("0.456")

val3 := createStakingValidator(ctx, mocks, 1, 3, 3)
pk3, _ := val3.CmtConsPublicKey()
valConsAddr3, _ := val3.GetConsAddr()
providerAddr3 := types.NewProviderConsAddress(valConsAddr3)
consumerValidator3 := types.ConsensusValidator{ProviderConsAddr: providerAddr3.ToSdkConsAddr(), Power: 3, PublicKey: &pk3}
val3.Tokens = sdk.TokensFromConsensusPower(3, sdk.DefaultPowerReduction)
val3.Description = stakingtypes.Description{Moniker: "ConsumerValidator3"}

mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(ctx, valConsAddr1).Return(val1, nil).AnyTimes()
mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(ctx, valConsAddr2).Return(val2, nil).AnyTimes()
mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(ctx, valConsAddr3).Return(val3, nil).AnyTimes()
mocks.MockStakingKeeper.EXPECT().PowerReduction(ctx).Return(sdk.DefaultPowerReduction).AnyTimes()
testkeeper.SetupMocksForLastBondedValidatorsExpectation(mocks.MockStakingKeeper, 2, []stakingtypes.Validator{val1, val2}, -1) // -1 to allow the calls "AnyTimes"

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

// expect no validator to be returned since the consumer is Opt-In
res, err = pk.QueryConsumerValidators(ctx, &req)
require.NoError(t, err)
require.Len(t, res.Validators, 0)

// opt in one validator
pk.SetOptedIn(ctx, consumerId, providerAddr1)

// set up the client id so the chain looks like it "started"
pk.SetConsumerClientId(ctx, consumerId, "clientID")
pk.SetConsumerValSet(ctx, consumerId, []types.ConsensusValidator{consumerValidator1, consumerValidator2})
// set a consumer commission rate for val1
val1ConsComRate := math.LegacyMustNewDecFromStr("0.456")
val1ConsComRate := math.LegacyMustNewDecFromStr("0.789")
pk.SetConsumerCommissionRate(ctx, consumerId, providerAddr1, val1ConsComRate)

expectedResponse := types.QueryConsumerValidatorsResponse{
// expect opted-in validator
res, err = pk.QueryConsumerValidators(ctx, &req)
require.NoError(t, err)
require.Len(t, res.Validators, 1)
require.Equal(t, res.Validators[0].ProviderAddress, providerAddr1.String())

// update consumer TopN param
pk.SetConsumerPowerShapingParameters(ctx, consumerId, types.PowerShapingParameters{Top_N: 50})

// expect both opted-in and topN validator
expRes := types.QueryConsumerValidatorsResponse{
Validators: []*types.QueryConsumerValidatorsValidator{
{
ProviderAddress: providerAddr1.String(),
Expand Down Expand Up @@ -168,22 +213,80 @@ func TestQueryConsumerValidators(t *testing.T) {
},
}

mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(ctx, valConsAddr1).Return(val1, nil).AnyTimes()
mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(ctx, valConsAddr2).Return(val2, nil).AnyTimes()
mocks.MockStakingKeeper.EXPECT().PowerReduction(ctx).Return(sdk.DefaultPowerReduction).AnyTimes()
// sort the address of the validators by ascending lexical order as they were persisted to the store
sort.Slice(expRes.Validators, func(i, j int) bool {
return bytes.Compare(
expRes.Validators[i].ConsumerKey.GetEd25519(),
expRes.Validators[j].ConsumerKey.GetEd25519(),
) == -1
})

testkeeper.SetupMocksForLastBondedValidatorsExpectation(mocks.MockStakingKeeper, 2, []stakingtypes.Validator{val1, val2}, -1) // -1 to allow the calls "AnyTimes"
res, err = pk.QueryConsumerValidators(ctx, &req)
require.NoError(t, err)
require.Equal(t, &expRes, res)

res, err := pk.QueryConsumerValidators(ctx, &req)
// expect same result when consumer is in "initialized" phase
pk.SetConsumerPhase(ctx, consumerId, types.ConsumerPhase_CONSUMER_PHASE_INITIALIZED)
res, err = pk.QueryConsumerValidators(ctx, &req)
require.NoError(t, err)
require.Equal(t, &expectedResponse, res)
require.Equal(t, &expRes, res)

// set consumer to the "launched" phase
pk.SetConsumerPhase(ctx, consumerId, types.ConsumerPhase_CONSUMER_PHASE_LAUNCHED)

// expect an empty consumer valset
// since neither QueueVSCPackets or MakeConsumerGenesis was called at this point
res, err = pk.QueryConsumerValidators(ctx, &req)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that from spawnTime till the actual launch, querying consumer validators would return an empty set? Is this what we want? @MSalopek

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Here we have a special case where the spawn time of the consumer chain has elapsed but has never gone through MakeConsumerGenesis - which should never happen in production.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With permissionless we could have 10k consumer chains wanting to spawn and because we launch the chain in the BeginBlock we probably intend to this in batches, e.g., 200 chains launch in block 1, the next 200 chains launch in block 2, etc. So, I guess the spawnTime might have passed but we have not executed MakeConsumerGenesis yet. Does that sound reasonable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the logic to check the consumer phase instead of the spawn time to avoid this issue.

require.NoError(t, err)
require.Empty(t, res)

// set consumer valset
pk.SetConsumerValSet(ctx, consumerId, []types.ConsensusValidator{
consumerValidator1,
consumerValidator2,
consumerValidator3,
})

expRes.Validators = append(expRes.Validators, &types.QueryConsumerValidatorsValidator{
ProviderAddress: providerAddr3.String(),
ConsumerKey: &pk3,
ConsumerPower: 3,
ConsumerCommissionRate: val3.Commission.Rate,
Description: val3.Description,
ProviderOperatorAddress: val3.OperatorAddress,
Jailed: val3.Jailed,
Status: val3.Status,
ProviderTokens: val3.Tokens,
ProviderCommissionRate: val3.Commission.Rate,
ProviderPower: 3,
ValidatesCurrentEpoch: true,
})

// sort the address of the validators by ascending lexical order as they were persisted to the store
sort.Slice(expRes.Validators, func(i, j int) bool {
return bytes.Compare(
expRes.Validators[i].ConsumerKey.GetEd25519(),
expRes.Validators[j].ConsumerKey.GetEd25519(),
) == -1
})

res, err = pk.QueryConsumerValidators(ctx, &req)
require.NoError(t, err)
require.Equal(t, &expRes, res)

// validator with no set consumer commission rate
pk.DeleteConsumerCommissionRate(ctx, consumerId, providerAddr1)
// because no consumer commission rate is set, the validator's set commission rate on the provider is used
res, err = pk.QueryConsumerValidators(ctx, &req)
require.NoError(t, err)
require.Equal(t, val1.Commission.Rate, res.Validators[0].ConsumerCommissionRate)

// set consumer to stopped phase
pk.SetConsumerPhase(ctx, consumerId, types.ConsumerPhase_CONSUMER_PHASE_STOPPED)
// expect empty valset
res, err = pk.QueryConsumerValidators(ctx, &req)
require.NoError(t, err)
require.Empty(t, res)
}

func TestQueryConsumerChainsValidatorHasToValidate(t *testing.T) {
Expand Down
54 changes: 2 additions & 52 deletions x/ccv/provider/types/legacy_proposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,15 @@ import (

const (
ProposalTypeConsumerAddition = "ConsumerAddition"
ProposalTypeConsumerRemoval = "ConsumerRemoval"
ProposalTypeConsumerModification = "ConsumerModification"
ProposalTypeConsumerRemoval = "RemoveConsumer"
ProposalTypeConsumerModification = "UpdateConsumer"
ProposalTypeEquivocation = "Equivocation"
ProposalTypeChangeRewardDenoms = "ChangeRewardDenoms"
)

var (
_ govv1beta1.Content = &ConsumerAdditionProposal{}
_ govv1beta1.Content = &ConsumerRemovalProposal{}
_ govv1beta1.Content = &ConsumerModificationProposal{}
_ govv1beta1.Content = &ChangeRewardDenomsProposal{}
_ govv1beta1.Content = &EquivocationProposal{}
)
Expand Down Expand Up @@ -240,55 +239,6 @@ func (sccp *ConsumerRemovalProposal) ValidateBasic() error {
return nil
}

// NewConsumerModificationProposal creates a new consumer modification proposal.
func NewConsumerModificationProposal(title, description, chainID string,
topN uint32,
validatorsPowerCap uint32,
validatorSetCap uint32,
allowlist []string,
denylist []string,
minStake uint64,
allowInactiveVals bool,
) govv1beta1.Content {
return &ConsumerModificationProposal{
Title: title,
Description: description,
ChainId: chainID,
Top_N: topN,
ValidatorsPowerCap: validatorsPowerCap,
ValidatorSetCap: validatorSetCap,
Allowlist: allowlist,
Denylist: denylist,
MinStake: minStake,
AllowInactiveVals: allowInactiveVals,
}
}

// ProposalRoute returns the routing key of a consumer modification proposal.
func (cccp *ConsumerModificationProposal) ProposalRoute() string { return RouterKey }

// ProposalType returns the type of the consumer modification proposal.
func (cccp *ConsumerModificationProposal) ProposalType() string {
return ProposalTypeConsumerModification
}

// ValidateBasic runs basic stateless validity checks
func (cccp *ConsumerModificationProposal) ValidateBasic() error {
if err := govv1beta1.ValidateAbstract(cccp); err != nil {
return err
}

if strings.TrimSpace(cccp.ChainId) == "" {
return errorsmod.Wrap(ErrInvalidConsumerModificationProposal, "consumer chain id must not be blank")
}

err := ValidatePSSFeatures(cccp.Top_N, cccp.ValidatorsPowerCap)
if err != nil {
return errorsmod.Wrapf(ErrInvalidConsumerModificationProposal, "invalid PSS features: %s", err.Error())
}
return nil
}

// NewEquivocationProposal creates a new equivocation proposal.
// [DEPRECATED]: do not use because equivocations can be submitted
// and verified automatically on the provider.
Expand Down
3 changes: 2 additions & 1 deletion x/ccv/provider/types/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package types
import (
"encoding/json"
"fmt"
cmttypes "github.com/cometbft/cometbft/types"
"strconv"
"strings"
"time"

cmttypes "github.com/cometbft/cometbft/types"

ibctmtypes "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"

errorsmod "cosmossdk.io/errors"
Expand Down
2 changes: 1 addition & 1 deletion x/ccv/provider/types/provider.pb.go

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

Loading