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!: compute partial set #1642

Closed

Conversation

insumity
Copy link
Contributor

@insumity insumity commented Feb 13, 2024

Description

Closes: #1606

This PR introduces the logic needed to compute the ValidatorUpdates in ComputeValidatorUpdates.


Author Checklist

All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.

I have...

  • Included the correct type prefix in the PR title
  • Added ! to the type prefix if the change is state-machine breaking
  • Confirmed this PR does not introduce changes requiring state migrations, OR migration code has been added to consumer and/or provider modules
  • Targeted the correct branch (see PR Targeting)
  • Provided a link to the relevant issue or specification
  • Followed the guidelines for building SDK modules
  • Included the necessary unit and integration tests
  • Added a changelog entry to CHANGELOG.md
  • Included comments for documenting Go code
  • Updated the relevant documentation or specification
  • Reviewed "Files changed" and left comments if necessary
  • Confirmed all CI checks have passed
  • If this PR is library API breaking, bump the go.mod version string of the repo, and follow through on a new major release

Reviewers Checklist

All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.

I have...

  • confirmed the correct type prefix in the PR title
  • confirmed ! the type prefix if the change is state-machine breaking
  • confirmed this PR does not introduce changes requiring state migrations, OR confirmed migration code has been added to consumer and/or provider modules
  • confirmed all author checklist items have been addressed
  • reviewed state machine logic
  • reviewed API design and naming
  • reviewed documentation is accurate
  • reviewed tests and test coverage

@github-actions github-actions bot added the C:x/provider Assigned automatically by the PR labeler label Feb 13, 2024
@insumity insumity changed the title Insumity/compute partial set feat!: compute partial set Feb 13, 2024
@@ -139,19 +139,30 @@ func (k msgServer) SubmitConsumerDoubleVoting(goCtx context.Context, msg *types.
func (k msgServer) OptIn(goCtx context.Context, msg *types.MsgOptIn) (*types.MsgOptInResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

valAddress, err := sdk.ConsAddressFromBech32(msg.ProviderAddr)
providerValidatorAddr, err := sdk.ValAddressFromBech32(msg.ProviderAddr)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed this. Similarly to AssignConsumerKey we consider that ProviderAddr in the message is the operator address and use this operator address to get the consensus address that is used later on.
We made a similar change in OptOut.

@@ -1190,14 +1190,17 @@ func (k Keeper) SetOptedIn(
chainID string,
providerAddr types.ProviderConsAddress,
blockHeight uint64,
power uint64,
Copy link
Contributor Author

@insumity insumity Feb 15, 2024

Choose a reason for hiding this comment

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

Introduce power in the opted-in state so we can see if we should send a ValidatorUpdate down to CometBFT based on whether the power changed since last time it was sent. Only makes sense in an epoch-based setting because in a non-epoch-based setting, we could use the validator updates from the staking module.

@insumity insumity marked this pull request as ready for review February 15, 2024 10:54
@insumity insumity requested a review from a team as a code owner February 15, 2024 10:54
@@ -255,6 +255,12 @@ func (k Keeper) MakeConsumerGenesis(
return false
})

// currentValidators := k.GetOptedIn(ctx, chainID)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Leaving those commented for now to show how ComputeValidatorUpdates, etc. are going to be used.

x/ccv/provider/keeper/proposal.go Outdated Show resolved Hide resolved
x/ccv/provider/keeper/proposal.go Show resolved Hide resolved
x/ccv/provider/keeper/keeper.go Show resolved Hide resolved
x/ccv/provider/keeper/partial_set_security.go Outdated Show resolved Hide resolved
x/ccv/provider/keeper/partial_set_security.go Outdated Show resolved Hide resolved
Comment on lines 237 to 240
k.DeleteAllOptedIn(ctx, chainID)
for _, val := range nextValidators {
k.SetOptedIn(ctx, chainID, val.ProviderAddr, val.BlockHeight, val.Power)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems very inneficient to delete all opten in entries and set the new ones. If in an epoch there will be no changes in the voting powers, this will be a very expensive way to leave the state unchanged.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is my understanding correct that at this stage, nothing is written to disk? Only if the transactions succeed, at the end all the changes would get written. If that's the case, why's this expensive? Wouldn't this correspond to a simple write in memory?
I agree that most likely the validator set won't change between epochs but probably it's unlikely that powers would remain the same between epochs for all validators, although this depends on how long an epoch is. Even in this case, I would need to loop through all currently opted in validators and compare with the nextValidators to see if a change took place.

Copy link
Contributor

Choose a reason for hiding this comment

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

might be premature optimization to worry about this? especially once we have epochs this happens rarely and it's at most ~200 entries being deleted and written per consumer

powerBytes := make([]byte, 8)
binary.BigEndian.PutUint64(powerBytes, power)

store.Set(types.OptedInKey(chainID, providerAddr), append(blockHeightBytes, powerBytes...))
Copy link
Contributor

Choose a reason for hiding this comment

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

Store OptedInValidator as a value (define it in the proto files).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is the recommendation here to define it in proto to simplify our life later on when we implement queries that return opted-in validators?

x/ccv/provider/keeper/partial_set_security.go Outdated Show resolved Hide resolved
x/ccv/provider/keeper/partial_set_security.go Outdated Show resolved Hide resolved

// ComputeNextValidators computes the next validator set that is responsible for validating on a consumer chain.
// The returned opted-in validators correspond to the next `currentValidators`.
func (k Keeper) ComputeNextValidators(ctx sdk.Context,
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel the logic in this function can be moved to ComputeValidatorUpdates.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's true. I believe that splitting this into two distinct methods makes it easier to read because the ValidateUpdates do not have a 1-to-1 correspodence with the next OptedInValidators.
But I don't have strong feelings on this and we can combine them as well.

Copy link
Contributor

@p-offtermatt p-offtermatt left a comment

Choose a reason for hiding this comment

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

Added a few comments. Having both ComputeValidatorUpdates and ComputeNextValidators feels weird to me - maybe we could reconsider the idea of taking diffs between new and old validator sets here

@@ -67,3 +72,152 @@ func (k Keeper) HandleOptOut(ctx sdk.Context, chainID string, providerAddr types

return nil
}

// ComputeValidatorUpdates computes the validator updates needed to be sent to the consumer chain to capture
// the newly opted-in and opted-out validators, as well as validators that unbonded.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// the newly opted-in and opted-out validators, as well as validators that unbonded.
// the newly opted-in and opted-out validators, as well as validators that started unbonding.

continue
}

// if `val` has unbonded, its `GetLastValidatorPower` power returns 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// if `val` has unbonded, its `GetLastValidatorPower` power returns 0
// if `val` has started unbonded, its `GetLastValidatorPower` power returns 0

I think there is subtle ambiguity with the current comments, since there are two things related to unbonding,
namely starting unbonding and finishing unbonding.
I think you referred to the start of unbonding here, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to refer to "not bonded" which means neither "unbonding" nor "unbonded." Technically, if a validator was bonded before and now "not bonded" anymore, it is in the "unbonding" phase. Might be easier to just state "not bonded" and that would be consistent with the check !validator.isBonded() I do in other places.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, that would be clearer I think

continue
}

// if the voting power did not change since the last time the validator opted in, do not create an update
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment does not seem correct to me - should this read

Suggested change
// if the voting power did not change since the last time the validator opted in, do not create an update
// if the voting power did not change between now and the last validator power, no need to add an update for this validator

?

Also, to better understand, with epochs, GetLastValidatorPower will need to be replaced with something like GetValidatorPowerAtLastEpoch, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The comment should probably be rephrased to:

// if the voting power is the same as the one the validator had when it opted in, do not create an update

because the voting power could change multiple times during an epoch, but at the end of an epoch, it could be that the validator has again the same voting power it had when it opted in.

Also, to better understand, with epochs, GetLastValidatorPower will need to be replaced with something like GetValidatorPowerAtLastEpoch, right?

Not sure I understand what you mean here. GetLastValidatorPower returns the most recent validator power and ComputeValidatorUpdates would be called at the end of every epoch.
Think of the following example. A validator V opts in at the end of an epoch e with power p. If at a later epoch e + X (X >= 1), validator V has still the same power p, then we should not send a ValidatorUpdate for V and this is what this checks here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right. GetLastValidatorPower will return the validator power in the last block. But what we need to compare against is the validator power at the last epoch. If epochLength==1, (as is the case right now) these coincide, but if epochLength != 1 this is not the case anymore.


// ComputeValidatorUpdates computes the validator updates needed to be sent to the consumer chain to capture
// the newly opted-in and opted-out validators, as well as validators that unbonded.
func (k Keeper) ComputeValidatorUpdates(ctx sdk.Context,
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like I am missing context so my judgement may not be accurate, but it feels like one of ComputeValidatorUpdates and ComputeNextValidators is unnecessary?
Wouldn't NextValidators + a diffing method like Jehan suggested a bit ago do the trick?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See my previous comment here.
More specific to the potentiality of a diff method, I guess you could first call ComputeNextValidators and then call diff(currentValidators, nextValidators) but this diff method would be equally complex ComputeValidatorUpdates and would have a different method signature compared to ComputeNextValidators. So, I don't see much the advantage to this.
Does that sound reasonable?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I agree. I think I would still find the diff method more elegant, but it probably comes down to personal preference, and at the end of the day both are ok.

Comment on lines 237 to 240
k.DeleteAllOptedIn(ctx, chainID)
for _, val := range nextValidators {
k.SetOptedIn(ctx, chainID, val.ProviderAddr, val.BlockHeight, val.Power)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

might be premature optimization to worry about this? especially once we have epochs this happens rarely and it's at most ~200 entries being deleted and written per consumer

@insumity insumity marked this pull request as draft February 19, 2024 08:16
@@ -279,7 +285,14 @@ func (k Keeper) MakeConsumerGenesis(
}

// Apply key assignments to the initial valset.
initialUpdatesWithConsumerKeys := k.MustApplyKeyAssignmentToValUpdates(ctx, chainID, initialUpdates)
initialUpdatesWithConsumerKeys := k.MustApplyKeyAssignmentToValUpdates(ctx, chainID, initialUpdates,
func(address types.ProviderConsAddress) bool {
Copy link
Contributor

@sainoe sainoe Feb 19, 2024

Choose a reason for hiding this comment

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

I think MustApplyKeyAssignmentToValUpdates will be reworked anyway to work with PSS.
But is it necessary to do this IsOptin check through a function argument?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C:x/provider Assigned automatically by the PR labeler
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants