From 3ea292371fc3ac8c182d13ff204189e083af6ade Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Wed, 28 Aug 2024 17:48:20 +0200 Subject: [PATCH] feat: extend `consumer_validators` query to return consumer valset before launch (#2164) * (partially) renamed chain ids to consumer ids * renamed proposal messages * removed global slash entry * fixed unit tests * added new messages * introduced new state * added functionality for the register and initialize messages * renamed (partially) chainIds to consumerIds * set consumerId to chainId association during registration * added extra check in the initialization so unknokwn, launched, or stopped chains cannot re-initialize * added initial work on traversing initialized chains that are to-be-launched * fixed rebase issues after bringing the VSCMaturedPackets work in * made it so we traverse initialization records instead of addition proposals (+ additional changes so the unit tests pass) * renamed more chainIDs to consumerIds * removed ClientIdToChainId state because chainId already resides on the registration record * nit fixes in go docs * removed MsgConsumerAddition * added CLI commands for new messages * removed consumer modification proposal * removed (partially) consumer removal proposal * rebased to pick up the inactive-validators work (PR #2079) * introduced consumerId in the equivocation messages (and a useful query for Hermes to get the consumerId) * added safeguard so that a validator cannot opt-in to two different chains with the same chain id * renamed some chainIDs to consumerIds * updated based on comments Co-authored-by: bernd-m <43466467+bermuell@users.noreply.github.com> * fixed integration tests * rebased to pick up the removal of legacy proposals (#2130) and re-introduced old messages so that existing proposals can deserialize * changes messages to only have MsgCreateConsumer and MsgUpdateConsumer and modified protos so that we are backward-compatible * cleaned up slightly a few things (mostly committing & pushing) so people can pick up the latest changes * fixed the CreateConsumer and UpdateConsumer logic and made most of the fields optional * fixed hooks and the code around proposalId to consumerId * pre-spawn query * rebase * nits * feat: extend consumer validator query to return commission rate (backport #2162) (#2165) * adapt #2162 changes for permissionless ICS * nits --------- Co-authored-by: kirdatatjana <116630536+kirdatatjana@users.noreply.github.com> * remove panics * renamed some chainIds to consumerIds * took into account comments and also added safeguard to reject new proposals that still use deprecated messages (e.g., MsgConsumerAddition, etc.) * Update x/ccv/provider/types/msg.go Co-authored-by: bernd-m <43466467+bermuell@users.noreply.github.com> * removed double-gas charge on MsgCreateConsumer and imroved the logic of MsgUpdateConsumer * added PopulateMinimumPowerInTopN tested * took into account comments (using protos for marshalling string slice, fixed issues in the UpdateConsumer logic, added extra check to abort spurious proposals) * update logic * took into account comments (using protos for marshalling string slice, fixed issues in the UpdateConsumer logic, added extra check to abort spurious proposals) * feat: add fields to consumer validators query (#2167) * extend consumer validators query * nit * nits * fix msg order * deprecate power for consumer_power * nits * feat: first iteration on Permissionless ICS (#2117) * (partially) renamed chain ids to consumer ids * renamed proposal messages * removed global slash entry * fixed unit tests * added new messages * introduced new state * added functionality for the register and initialize messages * renamed (partially) chainIds to consumerIds * set consumerId to chainId association during registration * added extra check in the initialization so unknokwn, launched, or stopped chains cannot re-initialize * added initial work on traversing initialized chains that are to-be-launched * fixed rebase issues after bringing the VSCMaturedPackets work in * made it so we traverse initialization records instead of addition proposals (+ additional changes so the unit tests pass) * renamed more chainIDs to consumerIds * removed ClientIdToChainId state because chainId already resides on the registration record * nit fixes in go docs * removed MsgConsumerAddition * added CLI commands for new messages * removed consumer modification proposal * removed (partially) consumer removal proposal * rebased to pick up the inactive-validators work (PR #2079) * introduced consumerId in the equivocation messages (and a useful query for Hermes to get the consumerId) * added safeguard so that a validator cannot opt-in to two different chains with the same chain id * renamed some chainIDs to consumerIds * updated based on comments Co-authored-by: bernd-m <43466467+bermuell@users.noreply.github.com> * fixed integration tests * rebased to pick up the removal of legacy proposals (#2130) and re-introduced old messages so that existing proposals can deserialize * changes messages to only have MsgCreateConsumer and MsgUpdateConsumer and modified protos so that we are backward-compatible * cleaned up slightly a few things (mostly committing & pushing) so people can pick up the latest changes * fixed the CreateConsumer and UpdateConsumer logic and made most of the fields optional * fixed hooks and the code around proposalId to consumerId * feat: extend consumer validator query to return commission rate (backport #2162) (#2165) * adapt #2162 changes for permissionless ICS * nits --------- Co-authored-by: kirdatatjana <116630536+kirdatatjana@users.noreply.github.com> * renamed some chainIds to consumerIds * took into account comments and also added safeguard to reject new proposals that still use deprecated messages (e.g., MsgConsumerAddition, etc.) * Update x/ccv/provider/types/msg.go Co-authored-by: bernd-m <43466467+bermuell@users.noreply.github.com> * removed double-gas charge on MsgCreateConsumer and imroved the logic of MsgUpdateConsumer * added PopulateMinimumPowerInTopN tested * took into account comments (using protos for marshalling string slice, fixed issues in the UpdateConsumer logic, added extra check to abort spurious proposals) * feat: add fields to consumer validators query (#2167) * extend consumer validators query * nit * nits * fix msg order * deprecate power for consumer_power * modified the way we verify the new owner address, as well as nit refactoring on the ConsumerIds * fixed some rebase issues and changed a proto to be backward-compatible --------- Co-authored-by: bernd-m <43466467+bermuell@users.noreply.github.com> Co-authored-by: Simon Noetzlin Co-authored-by: kirdatatjana <116630536+kirdatatjana@users.noreply.github.com> * cover stopped phase case * fixed bug on removing previous spawn time & added tests * added some additional tests * added tests on the hooks * removed check that spawn time is in the future * feat: refactor consumer validator set computation (#2175) * add UT * nits * address comments * Update x/ccv/provider/keeper/partial_set_security.go Co-authored-by: insumity * fix tests --------- Co-authored-by: insumity * nit * nits * nit --------- Co-authored-by: insumity Co-authored-by: bernd-m <43466467+bermuell@users.noreply.github.com> Co-authored-by: kirdatatjana <116630536+kirdatatjana@users.noreply.github.com> --- x/ccv/provider/keeper/grpc_query.go | 53 +++++++-- x/ccv/provider/keeper/grpc_query_test.go | 135 ++++++++++++++++++++--- x/ccv/provider/types/msg.go | 3 +- x/ccv/provider/types/provider.pb.go | 2 +- 4 files changed, 168 insertions(+), 25 deletions(-) diff --git a/x/ccv/provider/keeper/grpc_query.go b/x/ccv/provider/keeper/grpc_query.go index 4a167599ec..5562bfc2da 100644 --- a/x/ccv/provider/keeper/grpc_query.go +++ b/x/ccv/provider/keeper/grpc_query.go @@ -1,8 +1,10 @@ package keeper import ( + "bytes" "context" "fmt" + "sort" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -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 { + 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) + + // 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 { + return bytes.Compare( + consumerValSet[i].ProviderConsAddr, + consumerValSet[j].ProviderConsAddr, + ) == -1 + }) } + var validators []*types.QueryConsumerValidatorsValidator for _, consumerVal := range consumerValSet { provAddr := types.ProviderConsAddress{Address: consumerVal.ProviderConsAddr} consAddr := provAddr.ToSdkConsAddr() diff --git a/x/ccv/provider/keeper/grpc_query_test.go b/x/ccv/provider/keeper/grpc_query_test.go index bb51e85952..b44c025dd5 100644 --- a/x/ccv/provider/keeper/grpc_query_test.go +++ b/x/ccv/provider/keeper/grpc_query_test.go @@ -1,7 +1,9 @@ package keeper_test import ( + "bytes" "fmt" + "sort" "testing" "github.com/golang/mock/gomock" @@ -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(), @@ -168,15 +213,66 @@ 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) + 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) @@ -184,6 +280,13 @@ func TestQueryConsumerValidators(t *testing.T) { 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) { diff --git a/x/ccv/provider/types/msg.go b/x/ccv/provider/types/msg.go index 44466a1a75..08a4b516c9 100644 --- a/x/ccv/provider/types/msg.go +++ b/x/ccv/provider/types/msg.go @@ -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" diff --git a/x/ccv/provider/types/provider.pb.go b/x/ccv/provider/types/provider.pb.go index 4bd1ce4200..1c0755344d 100644 --- a/x/ccv/provider/types/provider.pb.go +++ b/x/ccv/provider/types/provider.pb.go @@ -37,7 +37,7 @@ var _ = time.Kitchen // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package -// ConsumerPhase indicates the phases of a consumer chain according to ADR 018 +// ConsumerPhase indicates the phases of a consumer chain according to ADR 019 type ConsumerPhase int32 const (