feat(proposer-set-reduction): Add proposer set state to x/staking#71
Conversation
|
@tqin7 your pull request is missing a changelog! |
e700587 to
d982fb5
Compare
d982fb5 to
8d8510f
Compare
|
|
||
| DelegationByValIndexKey = []byte{0x71} // key for delegations by a validator | ||
|
|
||
| ProposerKeyPrefix = []byte("Proposer:") // prefix for proposers (only a subset of validators can propose blocks) |
There was a problem hiding this comment.
To save space, let's use P:. We could also use hex like above, which carries a risk of colliding with future staking keys
There was a problem hiding this comment.
yeah went with a string to avoid future collision. changed this to P:
|
|
||
| if len(data.Proposers) == 0 { | ||
| // If no proposers are specified, set all validators as eligible proposers | ||
| for _, validator := range data.Validators { |
There was a problem hiding this comment.
We should have a "default state" where all active validators can propose.
Right now even though we have this default pre-genesis, it gets translated to an explicit list of proposers at genesis. This means that new validators that join the active set do not get to propose, even though "proposer set filter is not turned on". This is also important for the network upgrade - instead of setting the explicit list at the time of upgrade, you can just use this default state.
Two example ways to do this:
- If
ProposerKeyPrefixstore is empty, then "filter is off" and all active validators can propose - An additional boolean that defaults to false
There was a problem hiding this comment.
good idea! will go with filter is off if ProposerKeyPrefix store is empty
| } | ||
|
|
||
| // SetProposer sets a validator (given its operator address) as a block proposer. | ||
| func (k Keeper) SetProposer(ctx context.Context, operatorAddrStr string) error { |
There was a problem hiding this comment.
For security: let's add an invariant checker anytime proposer state is modified, that there's at least one validator that's able to propose blocks. We don't want to get into some state where no one is able to propose.
This shouldn't be expensive since we expect to change this state very infrequently.
There was a problem hiding this comment.
agreed. will check if given operator is a bonded validator
also thinking that in the gov msg that sets proposers, will check proposers list
| for ; iterator.Valid(); iterator.Next() { | ||
| key := iterator.Key() | ||
| // Skip prefix and length prefix to get actual address bytes | ||
| addrBytesWithLen := key[len(types.ProposerKeyPrefix):] |
There was a problem hiding this comment.
This logic seems error prone - are we following any existing code pattern that does this? Did we consider using storetypes.KVStorePrefixIterator?
There was a problem hiding this comment.
yeah. looked across cosmos-sdk and everywhere uses raw keys instead of a child store. and thus we need to parse these keys manually, which also is the pattern
however, i moved this address parsing logic to a helper function to make it clearer
| ) | ||
|
|
||
| // GetIsProposer returns whether a validator (given its operator address) is a block proposer. | ||
| func (k Keeper) GetIsProposer(ctx context.Context, operatorAddrStr string) (bool, error) { |
There was a problem hiding this comment.
Similar to below, this should also return true in the default state (no proposer set filter is active)
|
|
||
| // SetProposer sets a validator (given its operator address) as a block proposer. | ||
| func (k Keeper) SetProposer(ctx context.Context, operatorAddrStr string) error { | ||
| operatorAddr, err := k.validatorAddressCodec.StringToBytes(operatorAddrStr) |
There was a problem hiding this comment.
Does this return error if the input address is a valid but different form of address (eg consensus, delegator, etc)? Could we add test for this?
There was a problem hiding this comment.
yeah it returns error for any form other than operator address because k.validatorAddressCodec only handles operator addresses. added tests for this
| defer iterator.Close() | ||
|
|
||
| // Return true if not a single proposer is set | ||
| if !iterator.Valid() { |
There was a problem hiding this comment.
Are we sure this is the right condition? The method docstring simply says
// Valid returns whether the current iterator is valid. Once invalid, the Iterator remains
// invalid forever.
Valid() bool
Have we considered actually iterating throughe the iterator, keep a counter, and use counter = 0 as the condition?
| if err != nil { | ||
| return err | ||
| } | ||
| if validator.Status != types.Bonded { |
There was a problem hiding this comment.
Downside of this approach
- Gov vote starts to whitelist A, B, C as a proposer
- A short while before vote passes, A goes through a temporary outage and gets jailed
- One vote gets executed, the whole proposal fails since A is not bonded at that moment
This could be acceptable edge case. Possible alternative:
- Change this method to private
- Add a public interface
SetProposerssince the application never actually sets individual proposers, but just updates the list - At the end of public
SetProposers, call acheckInvariantfunction which verifies the following: callGetAllProposers, and check that at least1orXof them is bonded. This may be a more holistic approach, and also covers the check you were thinking to add for the gov msg.
There was a problem hiding this comment.
To confirm, once a validator gets jailed, it will start unbonding?
combining with your comment below, think that getting and setting the entire proposer list (instead of individual proposers) is sufficient and would cover these cases
|
|
||
| // GetIsProposer returns whether a validator (given its operator address) is a block proposer. | ||
| // Note: all validators are proposers if none is set. | ||
| func (k Keeper) GetIsProposer(ctx context.Context, operatorAddrStr string) (bool, error) { |
There was a problem hiding this comment.
Actually, how do we plan to use GetIsProposer for individual validator? I imagine this is somewhere in x/staking where you need to output the ValidateUpdate, and you need to augment the VP change with whether the validator is a proposer. Could we just call GetAllProposers, cache in memory, and use that in-memory object instead?
Not having to do GetIsProposer means you could just store one proto with a list of proposers addresses, which greatly reducing the complexity around working individual validator keys
| func (k Keeper) GetProposers(ctx context.Context) ([]string, error) { | ||
| store := k.storeService.OpenKVStore(ctx) | ||
|
|
||
| bz, err := store.Get(types.ProposerSetKey) |
There was a problem hiding this comment.
For my understanding, what does this return if types.ProposerSetKey is not touched at all yet?
|
|
||
| // Set proposers if specified in genesis. | ||
| // Note: if no proposer is set, all validators default to being proposers. | ||
| if len(data.Proposers) > 0 { |
There was a problem hiding this comment.
Did you run this locally from genesis? Wonder what happens if you call GetProposers before calling SetProposers at all? Trying to reason if a state migration is needed
There was a problem hiding this comment.
Yeah GetProposers just returns empty list (of strings)
in the abci method where x/staking creates validatorUpdates, i'm thinking that we call GetProposers, if it's empty, every ValidatorUpdate will set CanPropose=true, otherwise, set accordingly
There was a problem hiding this comment.
so yeah no state migration should be needed
|
|
||
| // SetProposers sets proposers in state by storing their operator addresses. | ||
| // Returns error if proposer set invariants are violated. | ||
| func (k Keeper) SetProposers(ctx context.Context, proposers []string) error { |
There was a problem hiding this comment.
You mentioned we tested locally through governance proposal - was this implemented somewhere else not in this PR?
There was a problem hiding this comment.
yep i have it locally and going to create another PR for it after this one is merged
|
|
||
| // key for proposer set | ||
| // Note: all validators default to being proposers if proposer set is empty | ||
| ProposerSetKey = []byte("ProposerSet") |
| } | ||
| } | ||
|
|
||
| return fmt.Errorf("at least one proposer must be bonded") |
There was a problem hiding this comment.
Instead of just a raw fmt.Errorf, consider wrapping an error code defined here. The error code can start at a different magnitude to avoid collision with upstream (eg 500, 501...)
| if err != nil { | ||
| return err | ||
| } | ||
| if validator.Status == types.Bonded { |
There was a problem hiding this comment.
I think we can generalize this by adding a code level constant MIN_BONDED_IN_PROPOSER SET, something like 5. Wdyt?
There was a problem hiding this comment.
sure! was trying to avoid hardcoding but think it's more robust than just requiring 1
fdf64ad to
958fa08
Compare
|
|
||
| const ( | ||
| // minimum number of bonded validators required in a proposer set | ||
| MIN_BONDED_IN_PROPOSER_SET = 5 |
|
|
||
| return fmt.Errorf("at least one proposer must be bonded") | ||
| if bonded < MIN_BONDED_IN_PROPOSER_SET { | ||
| return errorsmod.Wrapf(types.ErrInsufficientBondedValidators, |
* feat(proposer-set-reduction): Add proposer set state to x/staking (#71) * add proposer set state to x/staking * all validators are proposers when none is set and add more tests * get and set entire proposers list instead of each individual proposer * have a constant define min num of bonded in proposer set * fix lint and better error name * add gov msg that sets and query that reads proposer set (#72) * add gov msg that sets and query that reads proposer set * add comment on invariant checks * send proposer set updates in x/staking EndBlocker ABCI validator updates (#73) * send proposer set updates in x/staking EndBlocker ABCI validator updates * set full update flag in SetProposers keeper method and set CanPropose when constructing validator updates * update simapp's cometbft dependency * update cometbft dependency of /tests * add sanity check on full proposer set update * use cometbft commit on main (#74)
* feat(proposer-set-reduction): Add proposer set state to x/staking (dydxprotocol#71) * add proposer set state to x/staking * all validators are proposers when none is set and add more tests * get and set entire proposers list instead of each individual proposer * have a constant define min num of bonded in proposer set * fix lint and better error name * add gov msg that sets and query that reads proposer set (dydxprotocol#72) * add gov msg that sets and query that reads proposer set * add comment on invariant checks * send proposer set updates in x/staking EndBlocker ABCI validator updates (dydxprotocol#73) * send proposer set updates in x/staking EndBlocker ABCI validator updates * set full update flag in SetProposers keeper method and set CanPropose when constructing validator updates * update simapp's cometbft dependency * update cometbft dependency of /tests * add sanity check on full proposer set update * use cometbft commit on main (dydxprotocol#74)
Description
Add new state to x/staking that keeps track of which validators are eligible proposers. Changes are around genesis state, keys, and store.
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...
!to the type prefix if API or client breaking changeCHANGELOG.mdmake lintandmake testReviewers 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...
!in the type prefix if API or client breaking change