diff --git a/x/ccv/provider/keeper/keeper.go b/x/ccv/provider/keeper/keeper.go index ea052ec5fd..ea6562fef9 100644 --- a/x/ccv/provider/keeper/keeper.go +++ b/x/ccv/provider/keeper/keeper.go @@ -1209,6 +1209,19 @@ func (k Keeper) DeleteOptedIn( store.Delete(types.OptedInKey(chainID, providerAddr)) } +func (k Keeper) DeleteAllOptedIn( + ctx sdk.Context, + chainID string) { + store := ctx.KVStore(k.storeKey) + key := types.ChainIdWithLenKey(types.OptedInBytePrefix, chainID) + iterator := sdk.KVStorePrefixIterator(store, key) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + store.Delete(iterator.Key()) + } +} + func (k Keeper) IsOptedIn( ctx sdk.Context, chainID string, @@ -1254,6 +1267,19 @@ func (k Keeper) DeleteToBeOptedIn( store.Delete(types.ToBeOptedInKey(chainID, providerAddr)) } +func (k Keeper) DeleteAllToBeOptedIn( + ctx sdk.Context, + chainID string) { + store := ctx.KVStore(k.storeKey) + key := types.ChainIdWithLenKey(types.ToBeOptedInBytePrefix, chainID) + iterator := sdk.KVStorePrefixIterator(store, key) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + store.Delete(iterator.Key()) + } +} + func (k Keeper) IsToBeOptedIn( ctx sdk.Context, chainID string, @@ -1298,6 +1324,19 @@ func (k Keeper) DeleteToBeOptedOut( store.Delete(types.ToBeOptedOutKey(chainID, providerAddr)) } +func (k Keeper) DeleteAllToBeOptedOut( + ctx sdk.Context, + chainID string) { + store := ctx.KVStore(k.storeKey) + key := types.ChainIdWithLenKey(types.ToBeOptedOutBytePrefix, chainID) + iterator := sdk.KVStorePrefixIterator(store, key) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + store.Delete(iterator.Key()) + } +} + func (k Keeper) IsToBeOptedOut( ctx sdk.Context, chainID string, diff --git a/x/ccv/provider/keeper/key_assignment.go b/x/ccv/provider/keeper/key_assignment.go index fc864c6417..465157ce1e 100644 --- a/x/ccv/provider/keeper/key_assignment.go +++ b/x/ccv/provider/keeper/key_assignment.go @@ -554,6 +554,7 @@ func (k Keeper) MustApplyKeyAssignmentToValUpdates( ctx sdk.Context, chainID string, valUpdates []abci.ValidatorUpdate, + considerReplacement func(address types.ProviderConsAddress) bool, ) (newUpdates []abci.ValidatorUpdate) { for _, valUpdate := range valUpdates { providerAddrTmp, err := ccvtypes.TMCryptoPublicKeyToConsAddr(valUpdate.PubKey) @@ -608,6 +609,11 @@ func (k Keeper) MustApplyKeyAssignmentToValUpdates( // power in the pending key assignment. for _, replacement := range k.GetAllKeyAssignmentReplacements(ctx, chainID) { providerAddr := types.NewProviderConsAddress(replacement.ProviderAddr) + + // only consider updates for validators that are considered here ... + if !considerReplacement(providerAddr) { + return + } k.DeleteKeyAssignmentReplacement(ctx, chainID, providerAddr) newUpdates = append(newUpdates, abci.ValidatorUpdate{ PubKey: *replacement.PrevCKey, diff --git a/x/ccv/provider/keeper/key_assignment_test.go b/x/ccv/provider/keeper/key_assignment_test.go index 4fab08c981..e532fb0108 100644 --- a/x/ccv/provider/keeper/key_assignment_test.go +++ b/x/ccv/provider/keeper/key_assignment_test.go @@ -828,7 +828,7 @@ func TestSimulatedAssignmentsAndUpdateApplication(t *testing.T) { // and increment the provider vscid. applyUpdatesAndIncrementVSCID := func(updates []abci.ValidatorUpdate) { providerValset.apply(updates) - updates = k.MustApplyKeyAssignmentToValUpdates(ctx, CHAINID, updates) + updates = k.MustApplyKeyAssignmentToValUpdates(ctx, CHAINID, updates, func(address types.ProviderConsAddress) bool { return true }) consumerValset.apply(updates) // Simulate the VSCID update in EndBlock k.IncrementValidatorSetUpdateId(ctx) diff --git a/x/ccv/provider/keeper/msg_server.go b/x/ccv/provider/keeper/msg_server.go index 4863cd0d66..eaee4dfb8f 100644 --- a/x/ccv/provider/keeper/msg_server.go +++ b/x/ccv/provider/keeper/msg_server.go @@ -143,6 +143,7 @@ func (k msgServer) OptIn(goCtx context.Context, msg *types.MsgOptIn) (*types.Msg if err != nil { return nil, err } + // FIXME: something is off here .. val to cons ... providerAddr := types.NewProviderConsAddress(valAddress) if err != nil { return nil, err diff --git a/x/ccv/provider/keeper/partial_set_security.go b/x/ccv/provider/keeper/partial_set_security.go index e1bf9cc14f..ba6c371949 100644 --- a/x/ccv/provider/keeper/partial_set_security.go +++ b/x/ccv/provider/keeper/partial_set_security.go @@ -2,6 +2,8 @@ package keeper import ( errorsmod "cosmossdk.io/errors" + abci "github.com/cometbft/cometbft/abci/types" + tmprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/cosmos/interchain-security/v4/x/ccv/provider/types" @@ -20,6 +22,22 @@ func (k Keeper) HandleOptIn(ctx sdk.Context, chainID string, providerAddr types. "opting in to an unknown consumer chain, with id: %s", chainID) } + val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr()) + if !found { + return errorsmod.Wrapf( + types.ErrNoValidatorProviderAddress, + "could not find validator with consensus address: %s", providerAddr.ToSdkConsAddr().Bytes()) + } else if !val.IsBonded() { + // FIXME: problematic ... + // Only active validators are allowed to opt in. Note that the fact that a validator is bonded when sending + // a `MsgOptIn` message does not guarantee that the validator would still be bonded when the validator actually + // opts in (e.g., at the end of a block or of an epoch). We recheck if validators are bonded in + // `GetToBeOptedInValidatorUpdates` before sending the partial set to a consumer chain. + return errorsmod.Wrapf( + types.ErrValidatorNotBonded, + "validator with consensus address: %s is not bonded", providerAddr.ToSdkConsAddr().Bytes()) + } + if k.IsToBeOptedOut(ctx, chainID, providerAddr) { // a validator to be opted in cancels out with a validator to be opted out k.DeleteToBeOptedOut(ctx, chainID, providerAddr) @@ -67,3 +85,178 @@ func (k Keeper) HandleOptOut(ctx sdk.Context, chainID string, providerAddr types return nil } + +func (k Keeper) getValidatorsPublicKey(validator stakingtypes.Validator) (tmprotocrypto.PublicKey, error) { + consAddr, err := validator.GetConsAddr() + if err != nil { + return tmprotocrypto.PublicKey{}, err + } + return tmprotocrypto.PublicKey{ + Sum: &tmprotocrypto.PublicKey_Ed25519{ + Ed25519: consAddr.Bytes(), + }, + }, nil +} + +// GetToBeOptedInValidatorUpdates returns all the needed `ValidatorUpdate`s for validators that are to be opted in +// on consumer chain with `chainID` +func (k Keeper) GetToBeOptedInValidatorUpdates(ctx sdk.Context, chainID string) []abci.ValidatorUpdate { + var updates []abci.ValidatorUpdate + for _, val := range k.GetToBeOptedIn(ctx, chainID) { + validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, val.ToSdkConsAddr()) + if !found { + // a validator was successfully set to be opted in, but we cannot find this validator anymore + k.Logger(ctx).Error("could not find validator with consensus address: %s", val.ToSdkConsAddr().Bytes()) + } + + // FIXME: bonded means in the active ... + if !validator.IsBonded() { + // a validator might have started unbonding or unbonded since it asked to be opted in + continue + } + + pubKey, err := k.getValidatorsPublicKey(validator) + if err != nil { + k.Logger(ctx).Error("could not find validator with consensus address: %s", val.ToSdkConsAddr().Bytes()) + continue + } + + updates = append(updates, abci.ValidatorUpdate{ + PubKey: pubKey, + Power: k.stakingKeeper.GetLastValidatorPower(ctx, validator.GetOperator()), + }) + } + + return updates +} + +// GetToBeOptedOutValidatorUpdates returns all the needed `ValidatorUpdate`s for validators that are to be opted out +// of the consumer chain with `chainID` +func (k Keeper) GetToBeOptedOutValidatorUpdates(ctx sdk.Context, chainID string) []abci.ValidatorUpdate { + var updates []abci.ValidatorUpdate + for _, val := range k.GetToBeOptedOut(ctx, chainID) { + validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, val.ToSdkConsAddr()) + if !found { + // a validator was successfully set to be opted in, but we cannot find this validator anymore + k.Logger(ctx).Error("could not find validator with consensus address: %s", val.ToSdkConsAddr().Bytes()) + continue + } + + pubKey, err := k.getValidatorsPublicKey(validator) + if err != nil { + continue + } + + updates = append(updates, abci.ValidatorUpdate{ + PubKey: pubKey, + Power: 0, + }) + } + return updates +} + +// ComputePartialSetValUpdates computes and returns the partial set `ValidatorUpdate`s for given chain with `chainID` +// and provided the `stakingUpdates` stemming from the staking module +func (k Keeper) ComputePartialSetValUpdates(ctx sdk.Context, chainID string, stakingUpdates []abci.ValidatorUpdate) []abci.ValidatorUpdate { + var partialSetUpdates []abci.ValidatorUpdate + + toBeOptedInValidatorUpdates := k.GetToBeOptedInValidatorUpdates(ctx, chainID) + toBeOptedOutValidatorUpdates := k.GetToBeOptedOutValidatorUpdates(ctx, chainID) + + partialSetUpdates = append(partialSetUpdates, toBeOptedInValidatorUpdates...) + partialSetUpdates = append(partialSetUpdates, toBeOptedOutValidatorUpdates...) + + // Create set that contains all the validators that are to be opted out so that we do not reintroduce opted-out + // validators when going through the already opted in validators. Opted out validators are already included + // in the partial set updates through `toBeOptedOutValidatorUpdates`. + isSetToBeOptedOut := make(map[string]bool) + for _, val := range toBeOptedOutValidatorUpdates { + isSetToBeOptedOut[val.PubKey.String()] = true + } + + // Create set that contains all the validators that stem from `stakingUpdates` changes so that we only send + // validator updates for validators that had a change in their voting power. + isStakingUpdate := make(map[string]bool) + for _, val := range stakingUpdates { + isStakingUpdate[val.PubKey.String()] = true + } + + for _, val := range k.GetOptedIn(ctx, chainID) { + validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, val.ProviderAddr.ToSdkConsAddr()) + if !found { + continue + } + pubKey, err := k.getValidatorsPublicKey(validator) + if err != nil { + continue + } + + if isSetToBeOptedOut[pubKey.String()] { + // do not create a `ValidatorUpdate` for validators that opt out + continue + } + + if !isStakingUpdate[pubKey.String()] { + // only send an update if an opted in validator had a staking update from the staking module and + // hence a voting power change + continue + } + + partialSetUpdates = append(partialSetUpdates, abci.ValidatorUpdate{ + PubKey: pubKey, + Power: k.stakingKeeper.GetLastValidatorPower(ctx, validator.GetOperator()), + }) + } + + return partialSetUpdates +} + +func (k Keeper) ResetPartialSet(ctx sdk.Context, chainID string) { + var optedOutOnes map[string]bool + for _, v := range k.GetToBeOptedIn(ctx, chainID) { + optedOutOnes[v.String()] = true + } + + var allOfThem []types.ProviderConsAddress + for _, v := range k.GetOptedIn(ctx, chainID) { + // FOXME: + validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, v.ProviderAddr.ToSdkConsAddr()) + if !found { + // probably an error + continue + } + if !validator.IsBonded() { + continue + } + // only still bonded ones ... + if optedOutOnes[v.ProviderAddr.String()] { + // here you would need ot remove + continue + } + allOfThem = append(allOfThem, v.ProviderAddr) + } + + for _, v := range k.GetToBeOptedIn(ctx, chainID) { + // only still bonded ones ... + validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, v.ToSdkConsAddr()) + if !found { + // probably an error + continue + } + if !validator.IsBonded() { + continue + } + allOfThem = append(allOfThem, types.NewProviderConsAddress(v.Address)) + } + + for _, v := range allOfThem { + if !k.IsOptedIn(ctx, chainID, v) { + k.SetOptedIn(ctx, chainID, v, uint64(ctx.BlockHeight())) + } else { + // leave the previous block height as is + } + } + + k.DeleteAllToBeOptedIn(ctx, chainID) + k.DeleteAllToBeOptedOut(ctx, chainID) +} diff --git a/x/ccv/provider/keeper/partial_set_security_test.go b/x/ccv/provider/keeper/partial_set_security_test.go index 4831723bec..eab9feec04 100644 --- a/x/ccv/provider/keeper/partial_set_security_test.go +++ b/x/ccv/provider/keeper/partial_set_security_test.go @@ -1,6 +1,9 @@ package keeper_test import ( + "fmt" + abci "github.com/cometbft/cometbft/abci/types" + tmprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" sdk "github.com/cosmos/cosmos-sdk/types" @@ -10,15 +13,35 @@ import ( ccvtypes "github.com/cosmos/interchain-security/v4/x/ccv/types" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + "sort" "testing" ) func TestHandleOptIn(t *testing.T) { - providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() providerAddr := types.NewProviderConsAddress([]byte("providerAddr")) + // mock `GetValidatorByConsAddr` that returns a `Bonded` validator for the `providerAddr` address and + // an `Unbonding` validator for any other address + mocks.MockStakingKeeper.EXPECT(). + GetValidatorByConsAddr(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx sdk.Context, addr sdk.ConsAddress) (stakingtypes.Validator, bool) { + if addr.Equals(providerAddr.Address) { + pkAny, _ := codectypes.NewAnyWithValue(ed25519.GenPrivKeyFromSecret([]byte{1}).PubKey()) + return stakingtypes.Validator{ConsensusPubkey: pkAny, Status: stakingtypes.Bonded}, true + } else { + pkAny, _ := codectypes.NewAnyWithValue(ed25519.GenPrivKeyFromSecret([]byte{2}).PubKey()) + return stakingtypes.Validator{ConsensusPubkey: pkAny, Status: stakingtypes.Unbonding}, true + } + }).AnyTimes() + + // verify that a non-`Bonded` validator cannot opt in + unbondedProviderAddr := types.NewProviderConsAddress([]byte("unbondedProviderAddr")) + providerKeeper.SetProposedConsumerChain(ctx, "someChainID", 1) + require.Error(t, providerKeeper.HandleOptIn(ctx, "someChainID", unbondedProviderAddr, nil)) + // trying to opt in to a non-proposed and non-registered chain returns an error require.Error(t, providerKeeper.HandleOptIn(ctx, "unknownChainID", providerAddr, nil)) @@ -54,12 +77,12 @@ func TestHandleOptInWithConsumerKey(t *testing.T) { // Given `providerAddr`, `GetValidatorByConsAddr` returns a validator with the // exact same `ConsensusPubkey` pkAny, _ := codectypes.NewAnyWithValue(providerConsPubKey) - return stakingtypes.Validator{ConsensusPubkey: pkAny}, true + return stakingtypes.Validator{ConsensusPubkey: pkAny, Status: stakingtypes.Bonded}, true } else { // for any other consensus address, we cannot find a validator return stakingtypes.Validator{}, false } - }).Times(2), + }).Times(3), } gomock.InOrder(calls...) @@ -109,3 +132,263 @@ func TestHandleOptOut(t *testing.T) { require.NoError(t, err) require.False(t, providerKeeper.IsToBeOptedOut(ctx, "chainID", providerAddr)) } + +func TestGetToBeOptedInValidatorUpdates(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + type TestValidator struct { + validator stakingtypes.Validator + power int64 + expectedValUpdate abci.ValidatorUpdate + } + + chainID := "chainID" + + var testValidators []TestValidator + for i := int64(0); i < 10; i++ { + // generate a consensus public key for the provider + providerConsPubKey := ed25519.GenPrivKeyFromSecret([]byte{uint8(i)}).PubKey() + consAddr := sdk.ConsAddress(providerConsPubKey.Address()) + providerAddr := types.NewProviderConsAddress(consAddr) + + foo := providerAddr.Address.Bytes() + var providerValidatorAddr sdk.ValAddress + providerValidatorAddr = foo + + var status stakingtypes.BondStatus + if i%3 == 0 { + status = stakingtypes.Unbonded + } else if i%3 == 1 { + status = stakingtypes.Bonded + } else { + status = stakingtypes.Unbonding + } + + pkAny, _ := codectypes.NewAnyWithValue(providerConsPubKey) + validator := stakingtypes.Validator{ + OperatorAddress: providerValidatorAddr.String(), + ConsensusPubkey: pkAny, + Status: status, + } + + consumerTMPublicKey := tmprotocrypto.PublicKey{ + Sum: &tmprotocrypto.PublicKey_Ed25519{ + Ed25519: consAddr.Bytes(), + }, + } + + expectedValUpdate := abci.ValidatorUpdate{PubKey: consumerTMPublicKey, Power: i} + + testValidators = append(testValidators, + TestValidator{validator: validator, power: i, expectedValUpdate: expectedValUpdate}) + + providerKeeper.SetToBeOptedIn(ctx, chainID, providerAddr) + } + + for _, val := range testValidators { + consAddr, _ := val.validator.GetConsAddr() + mocks.MockStakingKeeper.EXPECT(). + GetValidatorByConsAddr(ctx, consAddr).Return(val.validator, true).AnyTimes() + mocks.MockStakingKeeper.EXPECT(). + GetLastValidatorPower(ctx, val.validator.GetOperator()).Return(val.power).AnyTimes() + } + + var expectedValUpdates []abci.ValidatorUpdate + for _, val := range testValidators { + if !val.validator.IsBonded() { + continue + } + expectedValUpdates = append(expectedValUpdates, val.expectedValUpdate) + } + + actualValUpdates := providerKeeper.GetToBeOptedInValidatorUpdates(ctx, chainID) + + // sort before comparing + sort.Slice(expectedValUpdates, func(i int, j int) bool { + if expectedValUpdates[i].PubKey.Compare(expectedValUpdates[j].PubKey) < 0 { + return true + } else if expectedValUpdates[i].PubKey.Compare(expectedValUpdates[j].PubKey) == 0 { + return expectedValUpdates[i].Power < expectedValUpdates[j].Power + } + return false + }) + sort.Slice(actualValUpdates, func(i int, j int) bool { + if actualValUpdates[i].PubKey.Compare(actualValUpdates[j].PubKey) < 0 { + return true + } else if actualValUpdates[i].PubKey.Compare(actualValUpdates[j].PubKey) == 0 { + return actualValUpdates[i].Power < actualValUpdates[j].Power + } + return false + }) + + require.Equal(t, expectedValUpdates, actualValUpdates) +} + +func TestGetToBeOptedOutValidatorUpdates(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + type TestValidator struct { + validator stakingtypes.Validator + power int64 + expectedValUpdate abci.ValidatorUpdate + } + + chainID := "chainID" + + var testValidators []TestValidator + for i := int64(0); i < 10; i++ { + // generate a consensus public key for the provider + providerConsPubKey := ed25519.GenPrivKeyFromSecret([]byte{uint8(i)}).PubKey() + consAddr := sdk.ConsAddress(providerConsPubKey.Address()) + providerAddr := types.NewProviderConsAddress(consAddr) + + foo := providerAddr.Address.Bytes() + var providerValidatorAddr sdk.ValAddress + providerValidatorAddr = foo + + pkAny, _ := codectypes.NewAnyWithValue(providerConsPubKey) + validator := stakingtypes.Validator{ + OperatorAddress: providerValidatorAddr.String(), + ConsensusPubkey: pkAny, + } + + consumerTMPublicKey := tmprotocrypto.PublicKey{ + Sum: &tmprotocrypto.PublicKey_Ed25519{ + Ed25519: consAddr.Bytes(), + }, + } + + expectedValUpdate := abci.ValidatorUpdate{PubKey: consumerTMPublicKey, Power: 0} + + testValidators = append(testValidators, + TestValidator{validator: validator, power: i, expectedValUpdate: expectedValUpdate}) + + providerKeeper.SetToBeOptedOut(ctx, chainID, providerAddr) + } + + for _, val := range testValidators { + consAddr, _ := val.validator.GetConsAddr() + mocks.MockStakingKeeper.EXPECT(). + GetValidatorByConsAddr(ctx, consAddr).Return(val.validator, true).AnyTimes() + } + + var expectedValUpdates []abci.ValidatorUpdate + for _, val := range testValidators { + expectedValUpdates = append(expectedValUpdates, val.expectedValUpdate) + } + + actualValUpdates := providerKeeper.GetToBeOptedOutValidatorUpdates(ctx, chainID) + + // sort before comparing + sort.Slice(expectedValUpdates, func(i int, j int) bool { + if expectedValUpdates[i].PubKey.Compare(expectedValUpdates[j].PubKey) < 0 { + return true + } else if expectedValUpdates[i].PubKey.Compare(expectedValUpdates[j].PubKey) == 0 { + return expectedValUpdates[i].Power < expectedValUpdates[j].Power + } + return false + }) + sort.Slice(actualValUpdates, func(i int, j int) bool { + if actualValUpdates[i].PubKey.Compare(actualValUpdates[j].PubKey) < 0 { + return true + } else if actualValUpdates[i].PubKey.Compare(actualValUpdates[j].PubKey) == 0 { + return actualValUpdates[i].Power < actualValUpdates[j].Power + } + return false + }) + + require.Equal(t, expectedValUpdates, actualValUpdates) +} + +func createValidators(bondStatutes []stakingtypes.BondStatus, powers []int64) (validators []stakingtypes.Validator, + valUpdates []abci.ValidatorUpdate) { + if len(bondStatutes) != len(powers) { + return + } + + for i := 0; i < len(bondStatutes); i++ { + providerConsPubKey := ed25519.GenPrivKeyFromSecret([]byte{uint8(i)}).PubKey() + consAddr := sdk.ConsAddress(providerConsPubKey.Address()) + providerAddr := types.NewProviderConsAddress(consAddr) + + var providerValidatorAddr sdk.ValAddress + providerValidatorAddr = providerAddr.Address.Bytes() + + pkAny, _ := codectypes.NewAnyWithValue(providerConsPubKey) + validator := stakingtypes.Validator{ + OperatorAddress: providerValidatorAddr.String(), + ConsensusPubkey: pkAny, + Status: bondStatutes[i], + } + validators = append(validators, validator) + + consumerTMPublicKey := tmprotocrypto.PublicKey{ + Sum: &tmprotocrypto.PublicKey_Ed25519{ + Ed25519: consAddr.Bytes(), + }, + } + + valUpdate := abci.ValidatorUpdate{PubKey: consumerTMPublicKey, Power: powers[i]} + valUpdates = append(valUpdates, valUpdate) + } + return +} + +func TestComputePartialValidatorUpdateSet(t *testing.T) { + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + type TestValidator struct { + validator stakingtypes.Validator + power int64 + expectedValUpdate abci.ValidatorUpdate + } + + chainID := "chainID" + + // create 10 validator updates + //var updates []abci.ValidatorUpdate + powers := []int64{1, 2, 3, 4, 5, 6} + vals, updates := createValidators( + []stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Unbonding, stakingtypes.Bonded, stakingtypes.Unbonded, + stakingtypes.Unbonded, stakingtypes.Bonded}, powers) + + addr0, _ := vals[0].GetConsAddr() + providerKeeper.SetOptedIn(ctx, chainID, types.NewProviderConsAddress(addr0), uint64(1)) + addr1, _ := vals[1].GetConsAddr() + providerKeeper.SetOptedIn(ctx, chainID, types.NewProviderConsAddress(addr1), uint64(2)) + addr2, _ := vals[2].GetConsAddr() + providerKeeper.SetOptedIn(ctx, chainID, types.NewProviderConsAddress(addr2), uint64(3)) + addr3, _ := vals[3].GetConsAddr() + providerKeeper.SetOptedIn(ctx, chainID, types.NewProviderConsAddress(addr3), uint64(4)) + + // set to be opted out + providerKeeper.SetToBeOptedOut(ctx, chainID, types.NewProviderConsAddress(addr2)) + providerKeeper.SetToBeOptedOut(ctx, chainID, types.NewProviderConsAddress(addr3)) + + // make all the validators at once ... kane to stakingUpdates based on the publickeys of those validators and then get the result + addr4, _ := vals[4].GetConsAddr() + providerKeeper.SetToBeOptedIn(ctx, chainID, types.NewProviderConsAddress(addr4)) + addr5, _ := vals[5].GetConsAddr() + providerKeeper.SetToBeOptedIn(ctx, chainID, types.NewProviderConsAddress(addr5)) + + for i, val := range vals { + consAddr, _ := val.GetConsAddr() + mocks.MockStakingKeeper.EXPECT(). + GetValidatorByConsAddr(ctx, consAddr).Return(val, true).AnyTimes() + mocks.MockStakingKeeper.EXPECT(). + GetLastValidatorPower(ctx, val.GetOperator()).Return(powers[i]).AnyTimes() + } + + actualValUpdates := providerKeeper.ComputePartialSetValUpdates(ctx, chainID, []abci.ValidatorUpdate{}) + + fmt.Println(updates) + fmt.Println(actualValUpdates) + //require.Equal(t, expectedValUpdates, actualValUpdates) +} + +func TestResetPartialSet(t *testing.T) { + // s +} diff --git a/x/ccv/provider/keeper/proposal.go b/x/ccv/provider/keeper/proposal.go index 50dc69d080..c391d72ddd 100644 --- a/x/ccv/provider/keeper/proposal.go +++ b/x/ccv/provider/keeper/proposal.go @@ -255,31 +255,38 @@ func (k Keeper) MakeConsumerGenesis( return false }) - initialUpdates := []abci.ValidatorUpdate{} - for _, p := range lastPowers { - addr, err := sdk.ValAddressFromBech32(p.Address) - if err != nil { - return gen, nil, err - } - - val, found := k.stakingKeeper.GetValidator(ctx, addr) - if !found { - return gen, nil, errorsmod.Wrapf(stakingtypes.ErrNoValidatorFound, "error getting validator from LastValidatorPowers: %s", err) - } - - tmProtoPk, err := val.TmConsPublicKey() - if err != nil { - return gen, nil, err - } - - initialUpdates = append(initialUpdates, abci.ValidatorUpdate{ - PubKey: tmProtoPk, - Power: p.Power, - }) - } + // at this initial state, no validator is yet opted in ... but a validator is to be opted in + initialUpdates := k.ComputePartialSetValUpdates(ctx, chainID, []abci.ValidatorUpdate{}) + + // + //initialUpdates := []abci.ValidatorUpdate{} + //for _, p := range lastPowers { + // addr, err := sdk.ValAddressFromBech32(p.Address) + // if err != nil { + // return gen, nil, err + // } + // + // validator, found := k.stakingKeeper.GetValidator(ctx, addr) + // if !found { + // return gen, nil, errorsmod.Wrapf(stakingtypes.ErrNoValidatorFound, "error getting validator from LastValidatorPowers: %s", err) + // } + // + // tmProtoPk, err := validator.TmConsPublicKey() + // if err != nil { + // return gen, nil, err + // } + // + // initialUpdates = append(initialUpdates, abci.ValidatorUpdate{ + // PubKey: tmProtoPk, + // Power: p.Power, + // }) + //} // Apply key assignments to the initial valset. - initialUpdatesWithConsumerKeys := k.MustApplyKeyAssignmentToValUpdates(ctx, chainID, initialUpdates) + initialUpdatesWithConsumerKeys := k.MustApplyKeyAssignmentToValUpdates(ctx, chainID, initialUpdates, + func(address types.ProviderConsAddress) bool { return k.IsToBeOptedIn(ctx, chainID, address) }) + + k.ResetPartialSet(ctx, chainID) // Get a hash of the consumer validator set from the update with applied consumer assigned keys updatesAsValSet, err := tmtypes.PB2TM.ValidatorUpdates(initialUpdatesWithConsumerKeys) @@ -360,6 +367,17 @@ func (k Keeper) BeginBlockInit(ctx sdk.Context) { propsToExecute := k.GetConsumerAdditionPropsToExecute(ctx) for _, prop := range propsToExecute { + // Only set Top N at the moment a chain starts. If we were to do this earlier (e.g., during the proposal), + // then someone could create a bogus ConsumerAdditionProposal to override the Top N for a specific chain. + k.SetTopN(ctx, prop.ChainId, prop.Top_N) + + if !k.IsTopN(ctx, prop.ChainId) && len(k.GetToBeOptedIn(ctx, prop.ChainId)) == 0 { + // drop the proposal + ctx.Logger().Info("could not start Opt In consumer chain (%s) because no validator has opted in", + prop.ChainId) + continue + } + // create consumer client in a cached context to handle errors cachedCtx, writeFn, err := k.CreateConsumerClientInCachedCtx(ctx, prop) if err != nil { @@ -368,10 +386,6 @@ func (k Keeper) BeginBlockInit(ctx sdk.Context) { continue } - // Only set Top N at the moment a chain starts. If we were to do this earlier (e.g., during the proposal), - // then someone could create a bogus ConsumerAdditionProposal to override the Top N for a specific chain. - k.SetTopN(ctx, prop.ChainId, prop.Top_N) - // The cached context is created with a new EventManager so we merge the event // into the original context ctx.EventManager().EmitEvents(cachedCtx.EventManager().Events()) diff --git a/x/ccv/provider/keeper/relay.go b/x/ccv/provider/keeper/relay.go index 2c00d40441..f090d367fe 100644 --- a/x/ccv/provider/keeper/relay.go +++ b/x/ccv/provider/keeper/relay.go @@ -218,8 +218,16 @@ func (k Keeper) QueueVSCPackets(ctx sdk.Context) { valUpdates := k.stakingKeeper.GetValidatorUpdates(ctx) for _, chain := range k.GetAllConsumerChains(ctx) { + partialSetUpdates := k.ComputePartialSetValUpdates(ctx, chain.ChainId, valUpdates) + + // FIXME: staking updtes contain ... ARE only for active set ... // Apply the key assignment to the validator updates. - valUpdates := k.MustApplyKeyAssignmentToValUpdates(ctx, chain.ChainId, valUpdates) + valUpdates := k.MustApplyKeyAssignmentToValUpdates(ctx, chain.ChainId, partialSetUpdates, + func(address providertypes.ProviderConsAddress) bool { + return k.IsOptedIn(ctx, chain.ChainId, address) + }) + + k.ResetPartialSet(ctx, chain.ChainId) // check whether there are changes in the validator set; // note that this also entails unbonding operations @@ -498,7 +506,7 @@ func (k Keeper) EndBlockCCR(ctx sdk.Context) { } } -// getMappedInfractionHeight gets the infraction height mapped from val set ID for the given chain ID +// getMappedInfractionHeight gets the infraction height mapped from validator set ID for the given chain ID func (k Keeper) getMappedInfractionHeight(ctx sdk.Context, chainID string, valsetUpdateID uint64, ) (height uint64, found bool) { diff --git a/x/ccv/provider/keeper/relay_test.go b/x/ccv/provider/keeper/relay_test.go index 02df262d53..f5955e63f0 100644 --- a/x/ccv/provider/keeper/relay_test.go +++ b/x/ccv/provider/keeper/relay_test.go @@ -385,7 +385,7 @@ func TestHandleSlashPacket(t *testing.T) { return testkeeper.GetMocksForHandleSlashPacket( ctx, mocks, providerConsAddr, // expected provider cons addr returned from GetProviderAddrFromConsumerAddr - stakingtypes.Validator{Jailed: false}, // staking keeper val to return + stakingtypes.Validator{Jailed: false}, // staking keeper validator to return true) // expectJailing = true }, 1, @@ -402,7 +402,7 @@ func TestHandleSlashPacket(t *testing.T) { return testkeeper.GetMocksForHandleSlashPacket( ctx, mocks, providerConsAddr, // expected provider cons addr returned from GetProviderAddrFromConsumerAddr - stakingtypes.Validator{Jailed: true}, // staking keeper val to return + stakingtypes.Validator{Jailed: true}, // staking keeper validator to return false) // expectJailing = false, validator is already jailed. }, 1, diff --git a/x/ccv/provider/keeper/throttle.go b/x/ccv/provider/keeper/throttle.go index b7e7fd5941..dc473ac506 100644 --- a/x/ccv/provider/keeper/throttle.go +++ b/x/ccv/provider/keeper/throttle.go @@ -17,7 +17,7 @@ import ( func (k Keeper) GetEffectiveValPower(ctx sdktypes.Context, valConsAddr providertypes.ProviderConsAddress, ) math.Int { - // Obtain staking module val object from the provider's consensus address. + // Obtain staking module validator object from the provider's consensus address. // Note: if validator is not found or unbonded, this will be handled appropriately in HandleSlashPacket val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, valConsAddr.ToSdkConsAddr()) diff --git a/x/ccv/provider/types/errors.go b/x/ccv/provider/types/errors.go index 6c19a7b396..b1d55a0ea1 100644 --- a/x/ccv/provider/types/errors.go +++ b/x/ccv/provider/types/errors.go @@ -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") + ErrValidatorNotBonded = errorsmod.Register(ModuleName, 19, "validator not bonded") )