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

Key assignment adds iteration on MsgCreateValidator #603

Closed
2 tasks done
Tracked by #1086
mpoke opened this issue Dec 15, 2022 · 18 comments · Fixed by #1279
Closed
2 tasks done
Tracked by #1086

Key assignment adds iteration on MsgCreateValidator #603

mpoke opened this issue Dec 15, 2022 · 18 comments · Fixed by #1279
Assignees
Labels
S: KTLO Keeping the lights on: Keeping the current product operational (bugs, troubleshooting, deps updates) S: NewThings Work towards your business objectives with new products, features, or integrations type: bug Issues that need priority attention -- something isn't working

Comments

@mpoke
Copy link
Contributor

mpoke commented Dec 15, 2022

Problem

Due to the key assignment protocol, when creating a new validator in the staking module (via the MsgCreateValidator message), the code iterates over all consumer keys which have been assigned in order to check if the validator being added is, or was, a consumer chain validator. This can be used as an attack vector.

Closing criteria

Figure out a way to get rid of this check.

Problem details

The ICS code implements the AfterValidatorCreated staking hook, which is called when a new validator is created. This hook iterates over all assigned consumer addresses, i.e.,

k.IterateAllValidatorsByConsumerAddr(ctx, func(_ string, consumerAddr sdk.ConsAddress, _ sdk.ConsAddress) (stop bool) {

TODOs

  • Labels have been added for issue
  • Issue has been added to the ICS project
@mpoke mpoke added type: bug Issues that need priority attention -- something isn't working iterators labels Dec 15, 2022
@jtremback
Copy link
Contributor

The loop iterates over all validators, which is the source of the problem. It does this because it is looking for a consumer address matching the new validator's address in an index with keys like chainId | consumerAddress. It can't just iterate over all chainIds and try to get the consumerAddress for each one because there could consumerAddress stored for chainIds that have not been created yet.

To solve this we could create a second index with keys like consumerAddress | chainId. This can then be used to check if an address is in use with one operation.

@mpoke
Copy link
Contributor Author

mpoke commented Dec 16, 2022

We should also look into whether is possible to have a single index with keys like consumerAddress | chainId. @danwt do you see any problems with that?

@danwt
Copy link
Contributor

danwt commented Dec 16, 2022

a second index with keys like consumerAddress | chainId

I think it works and is simple 👍

have a single index with keys like consumerAddress | chainId

I think this also works, and this could replace the current chainID | consumerAddress index because currently chain wise iteration is only done during DeleteKeyAssignments(ctx sdk.Context, chainID string) and this is a very rare operation.

@mpoke mpoke added this to the ICS v1.1.0 milestone Jan 27, 2023
@mpoke mpoke modified the milestones: ICS v1.1.0, ICS next release Mar 5, 2023
@mpoke mpoke modified the milestones: ICS next release, Gaia v10.0.0 Mar 13, 2023
@mpoke mpoke removed this from the Gaia v10.0.0 milestone Jun 8, 2023
@yaruwangway
Copy link
Contributor

yaruwangway commented Aug 25, 2023

Hi, the main is updated.
right now, if chainID is nil, GetAllValidatorsByConsumerAddr will get all the consensus addresses on all consumer chains.
It is a bit inefficient, when later looping through these addresses to get the CreateValidator. but what are the other issues this bring ?

If we change the index, from chainId | consumerAddress to consumerAddress | chainId, then it has to loop through all the addresses to check if the chainID is the chainID desired.
Instead, present len(chainID) | chainID is more efficient when getting addresses that ChainID not nil.

chainID, providerAddrTmp, err := types.ParseChainIdAndConsAddrKey(types.ConsumerValidatorsBytePrefix, iterator.Key())

@mpoke
Copy link
Contributor Author

mpoke commented Aug 25, 2023

GetAllValidatorsByConsumerAddr is called in three places:

  • ExportGenesis calls GetAllValidatorsByConsumerAddr(ctx, nil)
  • AfterValidatorCreated calls ValidatorConsensusKeyInUse(h.k, ctx, valAddr), which calls GetAllValidatorsByConsumerAddr(ctx, nil)
  • DeleteKeyAssignments calls k.GetAllValidatorsByConsumerAddr(ctx, &chainID)

Changing the index from chainId | consumerAddress to consumerAddress | chainId has the following effects:

  • The first call is not changed at all. Still need to iterate over all consumer addresses.
  • The second call requires no iteration, i.e.,
// ValidatorConsensusKeyInUse is called when a new validator is created
// in the x/staking module of cosmos-sdk. In case it panics, the TX aborts
// and thus, the validator is not created. See AfterValidatorCreated hook.
func ValidatorConsensusKeyInUse(k *Keeper, ctx sdk.Context, valAddr sdk.ValAddress) bool {
	// Get the validator being added in the staking module.
	val, found := k.stakingKeeper.GetValidator(ctx, valAddr)
	if !found {
		// Abort TX, do NOT allow validator to be created
		panic("did not find newly created validator in staking module")
	}

	// Get the consensus address of the validator being added
	consensusAddr, err := val.GetConsAddr()
	if err != nil {
		// Abort TX, do NOT allow validator to be created
		panic("could not get validator cons addr ")
	}

        store := ctx.KVStore(k.storeKey)
        prefix := ccvtypes.AppendMany(types.ValidatorsByConsumerAddrBytePrefix, consensusAddr)
        iterator := sdk.KVStorePrefixIterator(store, prefix)
	defer iterator.Close()

	for ; iterator.Valid(); iterator.Next() {
             return true
        }
        return false
}
  • The third call is less efficient as now we need to iterate over all consumer addresses and just remove the ones for a particular chain. This doesn't really matter though, since DeleteKeyAssignments is called only when a consumer chain is removed.

@yaruwangway
Copy link
Contributor

I see, in the above second call, when iterate the db to find the validatorConsumerAddrs, the consumer chainID is not known,(see below link the chainID aurgument is nil) , so that the prefix with |chainID| does not help fast find the addr, all the address with different chain-ID are returned. So chainId | consumerAddress and consumerAddress | chainId are the same in terms of iteration db.

for _, validatorConsumerAddrs := range k.GetAllValidatorsByConsumerAddr(ctx, nil) {

@yaruwangway
Copy link
Contributor

yaruwangway commented Sep 4, 2023

the validatorConsumerAddrs is stored in kvstore:
key: bytePrefix | len(chainID) | chainID | ConsAddress,
value: validatorConsumerAddrs, now we want to store

  • the migration will add the new key bytePrefix | ConsAddress|len(chainID) | chainID , value: validatorConsumerAddrs, so that a prefix bytePrefix | ConsAddress search can be used to fast find the added validator.
  • the migration will not delete the old key-value, the old key-value will be deleted in the next upgrade after the upgrade containing this migration.

@mpoke
Copy link
Contributor Author

mpoke commented Sep 5, 2023

  • the migration will not delete the old key-value, the old key-value will be deleted in the next upgrade after the upgrade containing this migration.

Why can't we delete the old key-value?

@yaruwangway
Copy link
Contributor

we can do all in one migration, i just feel it is secure if we still keep the old data for a bit longer ?

@MSalopek
Copy link
Contributor

MSalopek commented Sep 7, 2023

Just chiming in; it could be worthwhile to explore what cosmos-sdk collections bring us with regards to this issue.
There are some useful, more abstract types for handling data in the collections package that will be released in v50.

If this turns out to be too much trouble, let's consider using a collection if applicable and refactor this it in a later ICS version?

IndexedMap seems like a great abstraction for this usecase.

we can do all in one migration, i just feel it is secure if we still keep the old data for a bit longer ?

I'm not sure keeping old data adds to security.

@yaruwangway
Copy link
Contributor

yaruwangway commented Sep 8, 2023

Big thanks to @MSalopek for this migration free solution ! just check with trying each consumer ChainID ( for each chainID, if bytePrefix | len(chainID) | chainID | ConsAddress exists as key), there are not so many chains!

@MSalopek
Copy link
Contributor

MSalopek commented Sep 8, 2023

A note from today's dev session.

It seems that the iterations discussed above can be refactored such that the store key is calculated and simply accessed via store.Get that way we could check if the key is in use by making a <10 calls to the store instead of iterating over all consumer keys.

e.g.

// ValidatorConsensusKeyInUse is called when a new validator is created
// in the x/staking module of cosmos-sdk. In case it panics, the TX aborts
// and thus, the validator is not created. See AfterValidatorCreated hook.
func ValidatorConsensusKeyInUse(k *Keeper, ctx sdk.Context, valAddr sdk.ValAddress) bool {
	// Get the validator being added in the staking module.
	val, found := k.stakingKeeper.GetValidator(ctx, valAddr)
	if !found {
		// Abort TX, do NOT allow validator to be created
		panic("did not find newly created validator in staking module")
	}

	// Get the consensus address of the validator being added
	consensusAddr, err := val.GetConsAddr()
	if err != nil {
		// Abort TX, do NOT allow validator to be created
		panic("could not get validator cons addr ")
	}

	consuChains := k.GetAllConsumerChains(ctx)
	for _, consumerChain := range consuChains {
		if _, found := k.GetValidatorByConsumerAddr(
			ctx,
			consumerChain.ChainId,
			providertypes.NewConsumerConsAddress(consensusAddr)); found {
			return true
		}
	}

	return false
}

Additionally, a delete operation can be performed on the entire store subtree.

e.g.

// ChainIdWithLenKey returns the key with the following format:
// bytePrefix | len(chainID) | chainID
func ChainIdWithLenKey(prefix byte, chainID string) []byte {
	chainIdL := len(chainID)
	return ccvtypes.AppendMany(
		// Append the prefix
		[]byte{prefix},
		// Append the chainID length
		sdk.Uint64ToBigEndian(uint64(chainIdL)),
		// Append the chainID
		[]byte(chainID),
	)
}


// DeleteKeyAssignments deletes all the state needed for key assignments on a consumer chain
func (k Keeper) DeleteKeyAssignments(ctx sdk.Context, chainID string) {
	// delete ValidatorsByConsumerAddr
	for _, validatorConsumerAddr := range k.GetAllValidatorsByConsumerAddr(ctx, &chainID) {
		consumerAddr := types.NewConsumerConsAddress(validatorConsumerAddr.ConsumerAddr)
		k.DeleteValidatorByConsumerAddr(ctx, chainID, consumerAddr)
	}

        // replace with
        store.Delete(ctx, ChainWithLenKey(prefix, chainID)
        ...
}

Not sure about the delete, but by intuition I'd wager it can be done like that.

@mpoke
Copy link
Contributor Author

mpoke commented Sep 8, 2023

@MSalopek @yaruwangway It's not easy to iterate over all consumer chains as GetAllConsumerChains iterates only over registered consumers, i.e.,

// GetAllConsumerChains gets all of the consumer chains, for which the provider module
// created IBC clients. Consumer chains with created clients are also referred to as registered.

and consumer keys can be assigned for every chain, even those that are not yet registered, i.e.,

func (msg MsgAssignConsumerKey) ValidateBasic() error {
	if strings.TrimSpace(msg.ChainId) == "" {
		return ErrBlankConsumerChainID
	}
	// It is possible to assign keys for consumer chains that are not yet approved.
	// This can only be done by a signing validator, but it is still sensible
	// to limit the chainID size to prevent abuse.
	// TODO: In future, a mechanism will be added to limit assigning keys to chains
	// which are approved or pending approval, only.
	if 128 < len(msg.ChainId) {
		return ErrBlankConsumerChainID
	}

@yaruwangway
Copy link
Contributor

The loop iterates over all validators, which is the source of the problem. It does this because it is looking for a consumer address matching the new validator's address in an index with keys like chainId | consumerAddress. It can't just iterate over all chainIds and try to get the consumerAddress for each one because there could consumerAddress stored for chainIds that have not been created yet.

To solve this we could create a second index with keys like consumerAddress | chainId. This can then be used to check if an address is in use with one operation.

@jtremback , "It can't just iterate over all chainIds and try to get the consumerAddress for each one because there could consumerAddress stored for chainIds that have not been created yet." what does this mean ? cc @MSalopek

@MSalopek
Copy link
Contributor

MSalopek commented Sep 8, 2023

@yaruwangway It means that you can create a consumer key for a chain that is not yet registered (the gov proposal has not passed and the clientID is unknown).

@MSalopek
Copy link
Contributor

MSalopek commented Sep 8, 2023

@mpoke
Can we implement the TODO from the message above?

	// TODO: In future, a mechanism will be added to limit assigning keys to chains
	// which are approved or pending approval, only

@mpoke
Copy link
Contributor Author

mpoke commented Sep 8, 2023

@mpoke Can we implement the TODO from the message above?

	// TODO: In future, a mechanism will be added to limit assigning keys to chains
	// which are approved or pending approval, only

Sure. Please open a separate issue though.

@yaruwangway
Copy link
Contributor

yaruwangway commented Sep 8, 2023

just to clarify some edge cases :

  • is it possible chainID == "" in the store key: bytePrefix | len(chainID) | chainID | ConsAddress?
  • GetAllConsumerChains does not return chains with chainID == "" ?
  • GetAllConsumerChains might return chain1, chain2, but in the store key bytePrefix | len(chainID) | chainID | ConsAddress, the chainID might be chainX which is not in GetAllConsumerChains, so cannot use below code to find the addr in use ?
consuChains := k.GetAllConsumerChains(ctx)
	for _, consumerChain := range consuChains {
		if _, found := k.GetValidatorByConsumerAddr(
			ctx,
			consumerChain.ChainId,
			providertypes.NewConsumerConsAddress(consensusAddr)); found {
			return true
		}
	}

thanks!

@mpoke @MSalopek

@mpoke mpoke added the S: KTLO Keeping the lights on: Keeping the current product operational (bugs, troubleshooting, deps updates) label Sep 12, 2023
@yaruwangway yaruwangway assigned MSalopek and unassigned yaruwangway Oct 2, 2023
@MSalopek MSalopek added the S: NewThings Work towards your business objectives with new products, features, or integrations label Oct 9, 2023
@mpoke mpoke closed this as completed Mar 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S: KTLO Keeping the lights on: Keeping the current product operational (bugs, troubleshooting, deps updates) S: NewThings Work towards your business objectives with new products, features, or integrations type: bug Issues that need priority attention -- something isn't working
Projects
Status: ✅ Done
6 participants