Skip to content

Commit

Permalink
feat: add a query to retrieve validator set that was last sent to the…
Browse files Browse the repository at this point in the history
… consumer chain (#1867)

* init commit

* took into account comments

* add docs

* Update docs/docs/validators/partial-set-security-for-validators.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
insumity and coderabbitai[bot] authored May 17, 2024
1 parent 04afa78 commit e1b191a
Show file tree
Hide file tree
Showing 8 changed files with 1,163 additions and 139 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Introduces the `consumer-validators` query to retrieve the latest set consumer-validator set for a consumer chain.
([\#1863](https://github.com/cosmos/interchain-security/pull/1867))
19 changes: 18 additions & 1 deletion docs/docs/validators/partial-set-security-for-validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,30 @@ they will not be opted in automatically.
:::


### How to get all the opted-in validators on a consumer chain?
### How to retrieve all the opted-in validators on a consumer chain?
With the following query:
```bash
interchain-security-pd query provider consumer-opted-in-validators <consumer-chain-id>
```
we can see all the opted-in validators on `consumer-chain-id` that were manually or automatically opted in.

### How to retrieve all the consumer validators on a consumer chain?
With the following query:
```bash
interchain-security-pd query provider consumer-validators <consumer-chain-id>
```
we can see all the _consumer validators_ (i.e., validator set) of `consumer-chain-id`. The consumer validators are the
ones that are currently (or in the future, see warning) validating the consumer chain. A _consumer validator_ is an opted-in
validator but not vice versa. For example, an opted-in validator `V` might not be a consumer validator because `V` is
denylisted or because `V` is removed due to a validator-set cap.

:::warning
The returned consumer validators from this query do not necessarily correspond to the validator set that is
validating the consumer chain at this exact moment. This is because the `VSCPacket` sent to a consumer chain might be
delayed and hence this query might return the validator set that the consumer chain would have at some future
point in time.
:::

### How can we see the commission rate a validator has set on a consumer chain?
Using the following query:
```bash
Expand Down
26 changes: 25 additions & 1 deletion proto/interchain_security/ccv/provider/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ service Query {
option (google.api.http).get =
"/interchain_security/ccv/provider/oldest_unconfirmed_vsc/{chain_id}";
}

// QueryConsumerValidators returns the latest set consumer-validator set for a given chainID
// Note that this does not necessarily mean that the consumer chain is using this validator set at this exact moment
// because a VSCPacket could be delayed to be delivered on the consumer chain.
rpc QueryConsumerValidators(QueryConsumerValidatorsRequest)
returns (QueryConsumerValidatorsResponse) {
option (google.api.http).get =
"/interchain_security/ccv/provider/consumer_validators/{chain_id}";
}
}

message QueryConsumerGenesisRequest { string chain_id = 1; }
Expand Down Expand Up @@ -262,7 +271,6 @@ message QueryParamsResponse {
Params params = 1 [(gogoproto.nullable) = false];
}


message QueryConsumerChainOptedInValidatorsRequest {
string chain_id = 1;
}
Expand All @@ -272,6 +280,22 @@ message QueryConsumerChainOptedInValidatorsResponse {
repeated string validators_provider_addresses = 1;
}

message QueryConsumerValidatorsRequest {
string chain_id = 1;
}

message QueryConsumerValidatorsValidator {
// The consensus address of the validator on the provider chain
string provider_address = 1 [ (gogoproto.moretags) = "yaml:\"address\"" ];
// The consumer public key of the validator used on the consumer chain
tendermint.crypto.PublicKey consumer_key = 2;
// The power of the validator used on the consumer chain
int64 power = 3;
}

message QueryConsumerValidatorsResponse {
repeated QueryConsumerValidatorsValidator validators = 1;
}

message QueryConsumerChainsValidatorHasToValidateRequest {
// The consensus address of the validator on the provider chain
Expand Down
36 changes: 36 additions & 0 deletions x/ccv/provider/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func NewQueryCmd() *cobra.Command {
cmd.AddCommand(CmdAllPairsValConAddrByConsumerChainID())
cmd.AddCommand(CmdProviderParameters())
cmd.AddCommand(CmdConsumerChainOptedInValidators())
cmd.AddCommand(CmdConsumerValidators())
cmd.AddCommand(CmdConsumerChainsValidatorHasToValidate())
cmd.AddCommand(CmdValidatorConsumerCommissionRate())
cmd.AddCommand(CmdOldestUnconfirmedVsc())
Expand Down Expand Up @@ -448,6 +449,41 @@ $ %s consumer-opted-in-validators foochain
return cmd
}

// Command to query the consumer validators by consumer chain ID
func CmdConsumerValidators() *cobra.Command {
cmd := &cobra.Command{
Use: "consumer-validators [chainid]",
Short: "Query the last set consumer-validator set for a given consumer chain",
Long: strings.TrimSpace(
fmt.Sprintf(`Query the last set consumer-validator set for a given consumer chain.
Note that this does not necessarily mean that the consumer chain is currently using this validator set because a VSCPacket could be delayed, etc.
Example:
$ %s consumer-validators foochain
`, version.AppName),
),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)

res, err := queryClient.QueryConsumerValidators(cmd.Context(),
&types.QueryConsumerValidatorsRequest{ChainId: args[0]})
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)

return cmd
}

// Command to query the consumer chains list a given validator has to validate
func CmdConsumerChainsValidatorHasToValidate() *cobra.Command {
bech32PrefixConsAddr := sdk.GetConfig().GetBech32ConsensusAddrPrefix()
Expand Down
32 changes: 32 additions & 0 deletions x/ccv/provider/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,38 @@ func (k Keeper) QueryConsumerChainOptedInValidators(goCtx context.Context, req *
}, nil
}

// QueryConsumerValidators returns all validators that are consumer validators in a given consumer chain
func (k Keeper) QueryConsumerValidators(goCtx context.Context, req *types.QueryConsumerValidatorsRequest) (*types.QueryConsumerValidatorsResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}

consumerChainID := req.ChainId
if consumerChainID == "" {
return nil, status.Error(codes.InvalidArgument, "empty chainId")
}

ctx := sdk.UnwrapSDKContext(goCtx)

if _, found := k.GetConsumerClientId(ctx, consumerChainID); !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", consumerChainID))
}

var validators []*types.QueryConsumerValidatorsValidator
for _, v := range k.GetConsumerValSet(ctx, consumerChainID) {
validators = append(validators, &types.QueryConsumerValidatorsValidator{
ProviderAddress: sdk.ConsAddress(v.ProviderConsAddr).String(),
ConsumerKey: v.ConsumerPublicKey,
Power: v.Power,
})
}

return &types.QueryConsumerValidatorsResponse{
Validators: validators,
}, nil
}

// QueryConsumerChainsValidatorHasToValidate returns all consumer chains that the given validator has to validate now
// or in the next epoch if nothing changes.
func (k Keeper) QueryConsumerChainsValidatorHasToValidate(goCtx context.Context, req *types.QueryConsumerChainsValidatorHasToValidateRequest) (*types.QueryConsumerChainsValidatorHasToValidateResponse, error) {
Expand Down
146 changes: 146 additions & 0 deletions x/ccv/provider/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package keeper_test

import (
"github.com/cometbft/cometbft/proto/tendermint/crypto"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"testing"
"time"

Expand Down Expand Up @@ -103,3 +105,147 @@ func TestQueryOldestUnconfirmedVsc(t *testing.T) {
}
require.Equal(t, expectedResult, response.VscSendTimestamp)
}

func TestQueryConsumerChainOptedInValidators(t *testing.T) {
chainID := "chainID"

pk, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t))
defer ctrl.Finish()

req := types.QueryConsumerChainOptedInValidatorsRequest{
ChainId: chainID,
}

// error returned from not yet proposed or not yet registered chain
_, err := pk.QueryConsumerChainOptedInValidators(ctx, &req)
require.Error(t, err)

pk.SetProposedConsumerChain(ctx, chainID, 1)

providerAddr1 := types.NewProviderConsAddress([]byte("providerAddr1"))
providerAddr2 := types.NewProviderConsAddress([]byte("providerAddr2"))
expectedResponse := types.QueryConsumerChainOptedInValidatorsResponse{
ValidatorsProviderAddresses: []string{providerAddr1.String(), providerAddr2.String()},
}

pk.SetOptedIn(ctx, chainID, providerAddr1)
pk.SetOptedIn(ctx, chainID, providerAddr2)
res, err := pk.QueryConsumerChainOptedInValidators(ctx, &req)
require.NoError(t, err)
require.Equal(t, &expectedResponse, res)
}

func TestQueryConsumerValidators(t *testing.T) {
chainID := "chainID"

pk, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t))
defer ctrl.Finish()

req := types.QueryConsumerValidatorsRequest{
ChainId: chainID,
}

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

providerAddr1 := types.NewProviderConsAddress([]byte("providerAddr1"))
consumerKey1 := cryptotestutil.NewCryptoIdentityFromIntSeed(1).TMProtoCryptoPublicKey()
consumerValidator1 := types.ConsumerValidator{ProviderConsAddr: providerAddr1.ToSdkConsAddr(), Power: 1, ConsumerPublicKey: &consumerKey1}

providerAddr2 := types.NewProviderConsAddress([]byte("providerAddr2"))
consumerKey2 := cryptotestutil.NewCryptoIdentityFromIntSeed(2).TMProtoCryptoPublicKey()
consumerValidator2 := types.ConsumerValidator{ProviderConsAddr: providerAddr2.ToSdkConsAddr(), Power: 2, ConsumerPublicKey: &consumerKey2}

expectedResponse := types.QueryConsumerValidatorsResponse{
Validators: []*types.QueryConsumerValidatorsValidator{
{providerAddr1.String(), &consumerKey1, 1},
{providerAddr2.String(), &consumerKey2, 2},
},
}

// set up the client id so the chain looks like it "started"
pk.SetConsumerClientId(ctx, chainID, "clientID")
pk.SetConsumerValSet(ctx, chainID, []types.ConsumerValidator{consumerValidator1, consumerValidator2})

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

func TestQueryConsumerChainsValidatorHasToValidate(t *testing.T) {
pk, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t))
defer ctrl.Finish()

val := createStakingValidator(ctx, mocks, 1, 1)
valConsAddr, _ := val.GetConsAddr()
providerAddr := types.NewProviderConsAddress(valConsAddr)
mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(ctx, valConsAddr).Return(val, true).AnyTimes()
mocks.MockStakingKeeper.EXPECT().GetLastValidators(ctx).Return([]stakingtypes.Validator{val}).AnyTimes()

req := types.QueryConsumerChainsValidatorHasToValidateRequest{
ProviderAddress: providerAddr.String(),
}

// set up some consumer chains
consumerChains := []string{"chain1", "chain2", "chain3", "chain4"}
for _, cc := range consumerChains {
pk.SetConsumerClientId(ctx, cc, "clientID")
}

// set `providerAddr` as a consumer validator on "chain1"
pk.SetConsumerValidator(ctx, "chain1", types.ConsumerValidator{
ProviderConsAddr: providerAddr.ToSdkConsAddr(),
Power: 1,
ConsumerPublicKey: &crypto.PublicKey{
Sum: &crypto.PublicKey_Ed25519{
Ed25519: []byte{1},
}}})

// set `providerAddr` as an opted-in validator on "chain3"
pk.SetOptedIn(ctx, "chain3", providerAddr)

// `providerAddr` has to validate "chain1" because it is a consumer validator in this chain, as well as "chain3"
// because it opted in, in "chain3" and `providerAddr` belongs to the bonded validators (see the mocking of `GetLastValidators`
// above)
expectedChains := []string{"chain1", "chain3"}

res, err := pk.QueryConsumerChainsValidatorHasToValidate(ctx, &req)
require.NoError(t, err)
require.Equal(t, expectedChains, res.ConsumerChainIds)
}

func TestQueryValidatorConsumerCommissionRate(t *testing.T) {
chainID := "chainID"

pk, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t))
defer ctrl.Finish()

providerAddr := types.NewProviderConsAddress([]byte("providerAddr"))
req := types.QueryValidatorConsumerCommissionRateRequest{
ChainId: chainID,
ProviderAddress: providerAddr.String(),
}

// error returned from not yet proposed or not yet registered chain
_, err := pk.QueryValidatorConsumerCommissionRate(ctx, &req)
require.Error(t, err)

pk.SetProposedConsumerChain(ctx, chainID, 1)
// validator with set consumer commission rate
expectedCommissionRate, _ := sdktypes.NewDecFromStr("0.123")
pk.SetConsumerCommissionRate(ctx, chainID, providerAddr, expectedCommissionRate)
res, _ := pk.QueryValidatorConsumerCommissionRate(ctx, &req)
require.Equal(t, expectedCommissionRate, res.Rate)

// validator with no set consumer commission rate
pk.DeleteConsumerCommissionRate(ctx, chainID, providerAddr)
expectedCommissionRate, _ = sdktypes.NewDecFromStr("0.456")

// because no consumer commission rate is set, the validator's set commission rate on the provider is used
val := stakingtypes.Validator{Commission: stakingtypes.Commission{CommissionRates: stakingtypes.CommissionRates{Rate: expectedCommissionRate}}}
mocks.MockStakingKeeper.EXPECT().GetValidatorByConsAddr(
ctx, providerAddr.ToSdkConsAddr()).Return(val, true).Times(1)
res, _ = pk.QueryValidatorConsumerCommissionRate(ctx, &req)
require.Equal(t, expectedCommissionRate, res.Rate)
}
Loading

0 comments on commit e1b191a

Please sign in to comment.