diff --git a/CHANGELOG.md b/CHANGELOG.md index f9a6f041be5c..ea96f6dbd5aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,9 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### API Breaking Changes +* (x/staking) [#17562](https://github.com/cosmos/cosmos-sdk/pull/17562) Use collections for `ValidatorQueue` + * remove from `types`: `GetValidatorQueueKey`, `ParseValidatorQueueKey` + * remove from `Keeper`: `ValidatorQueueIterator` * (x/staking) [#17498](https://github.com/cosmos/cosmos-sdk/pull/17498) Use collections for `LastValidatorPower`: * remove from `types`: `GetLastValidatorPowerKey` * remove from `Keeper`: `LastValidatorsIterator`, `IterateLastValidators` diff --git a/x/staking/keeper/keeper.go b/x/staking/keeper/keeper.go index 2b17c16ee882..c84be8549af7 100644 --- a/x/staking/keeper/keeper.go +++ b/x/staking/keeper/keeper.go @@ -68,6 +68,8 @@ type Keeper struct { RedelegationsByValSrc collections.Map[collections.Triple[[]byte, []byte, []byte], []byte] // UnbondingDelegationByValIndex key: valAddr+delAddr | value: none used (index key for UnbondingDelegations stored by validator index) UnbondingDelegationByValIndex collections.Map[collections.Pair[[]byte, []byte], []byte] + // ValidatorQueue key: len(timestamp bytes)+timestamp+height | value: ValAddresses + ValidatorQueue collections.Map[collections.Triple[uint64, time.Time, uint64], types.ValAddresses] // LastValidatorPower key: valAddr | value: power(gogotypes.Int64Value()) LastValidatorPower collections.Map[[]byte, gogotypes.Int64Value] } @@ -186,7 +188,20 @@ func NewKeeper( collections.BytesKey, sdk.LengthPrefixedBytesKey, // sdk.LengthPrefixedBytesKey is needed to retain state compatibility ), - codec.CollValue[types.UnbondingDelegation](cdc)), + codec.CollValue[types.UnbondingDelegation](cdc), + ), + // key format is: 67 | length(timestamp Bytes) | timestamp | height + // Note: We use 3 keys here because we prefixed time bytes with its length previously and to retain state compatibility we remain to use the same + ValidatorQueue: collections.NewMap( + sb, types.ValidatorQueueKey, + "validator_queue", + collections.TripleKeyCodec( + collections.Uint64Key, + sdk.TimeKey, + collections.Uint64Key, + ), + codec.CollValue[types.ValAddresses](cdc), + ), } schema, err := sb.Build() diff --git a/x/staking/keeper/keeper_test.go b/x/staking/keeper/keeper_test.go index 6266960a827e..e5f1052893c3 100644 --- a/x/staking/keeper/keeper_test.go +++ b/x/staking/keeper/keeper_test.go @@ -202,6 +202,33 @@ func getLastValidatorPowerKey(operator sdk.ValAddress) []byte { return append(lastValidatorPowerKey, addresstypes.MustLengthPrefix(operator)...) } +// getValidatorQueueKey returns the prefix key used for getting a set of unbonding +// validators whose unbonding completion occurs at the given time and height. +func getValidatorQueueKey(timestamp time.Time, height int64) []byte { + validatorQueueKey := []byte{0x43} + + heightBz := sdk.Uint64ToBigEndian(uint64(height)) + timeBz := sdk.FormatTimeBytes(timestamp) + timeBzL := len(timeBz) + prefixL := len(validatorQueueKey) + + bz := make([]byte, prefixL+8+timeBzL+8) + + // copy the prefix + copy(bz[:prefixL], validatorQueueKey) + + // copy the encoded time bytes length + copy(bz[prefixL:prefixL+8], sdk.Uint64ToBigEndian(uint64(timeBzL))) + + // copy the encoded time bytes + copy(bz[prefixL+8:prefixL+8+timeBzL], timeBz) + + // copy the encoded height + copy(bz[prefixL+8+timeBzL:], heightBz) + + return bz +} + func (s *KeeperTestSuite) TestLastTotalPowerMigrationToColls() { s.SetupTest() @@ -445,6 +472,44 @@ func (s *KeeperTestSuite) TestValidatorsMigrationToColls() { s.Require().NoError(err) } +func (s *KeeperTestSuite) TestValidatorQueueMigrationToColls() { + s.SetupTest() + _, valAddrs := createValAddrs(100) + endTime := time.Unix(0, 0).UTC() + endHeight := int64(10) + err := testutil.DiffCollectionsMigration( + s.ctx, + s.key, + 100, + func(i int64) { + var addrs []string + addrs = append(addrs, valAddrs[i].String()) + bz, err := s.cdc.Marshal(&stakingtypes.ValAddresses{Addresses: addrs}) + s.Require().NoError(err) + + // legacy Set method + s.ctx.KVStore(s.key).Set(getValidatorQueueKey(endTime, endHeight), bz) + }, + "8cf5fb4def683e83ea4cc4f14d8b2abc4c6af66709ad8af391dc749e63ef7524", + ) + s.Require().NoError(err) + + err = testutil.DiffCollectionsMigration( + s.ctx, + s.key, + 100, + func(i int64) { + var addrs []string + addrs = append(addrs, valAddrs[i].String()) + + err := s.stakingKeeper.SetUnbondingValidatorsQueue(s.ctx, endTime, endHeight, addrs) + s.Require().NoError(err) + }, + "8cf5fb4def683e83ea4cc4f14d8b2abc4c6af66709ad8af391dc749e63ef7524", + ) + s.Require().NoError(err) +} + func TestKeeperTestSuite(t *testing.T) { suite.Run(t, new(KeeperTestSuite)) } diff --git a/x/staking/keeper/validator.go b/x/staking/keeper/validator.go index fb82f1929a79..bc8b028c17a0 100644 --- a/x/staking/keeper/validator.go +++ b/x/staking/keeper/validator.go @@ -20,6 +20,8 @@ import ( "github.com/cosmos/cosmos-sdk/x/staking/types" ) +var timeBzKeySize = uint64(29) // time bytes key size is 29 by default + // GetValidator gets a single validator func (k Keeper) GetValidator(ctx context.Context, addr sdk.ValAddress) (validator types.Validator, err error) { validator, err = k.Validators.Get(ctx, addr) @@ -403,34 +405,20 @@ func (k Keeper) GetLastValidators(ctx context.Context) (validators []types.Valid // GetUnbondingValidators returns a slice of mature validator addresses that // complete their unbonding at a given time and height. func (k Keeper) GetUnbondingValidators(ctx context.Context, endTime time.Time, endHeight int64) ([]string, error) { - store := k.storeService.OpenKVStore(ctx) - - bz, err := store.Get(types.GetValidatorQueueKey(endTime, endHeight)) - if err != nil { - return nil, err - } - - if bz == nil { - return []string{}, nil - } - - addrs := types.ValAddresses{} - if err = k.cdc.Unmarshal(bz, &addrs); err != nil { - return nil, err + timeSize := sdk.TimeKey.Size(endTime) + valAddrs, err := k.ValidatorQueue.Get(ctx, collections.Join3(uint64(timeSize), endTime, uint64(endHeight))) + if err != nil && !errors.Is(err, collections.ErrNotFound) { + return []string{}, err } - return addrs.Addresses, nil + return valAddrs.Addresses, nil } // SetUnbondingValidatorsQueue sets a given slice of validator addresses into // the unbonding validator queue by a given height and time. func (k Keeper) SetUnbondingValidatorsQueue(ctx context.Context, endTime time.Time, endHeight int64, addrs []string) error { - store := k.storeService.OpenKVStore(ctx) - bz, err := k.cdc.Marshal(&types.ValAddresses{Addresses: addrs}) - if err != nil { - return err - } - return store.Set(types.GetValidatorQueueKey(endTime, endHeight), bz) + valAddrs := types.ValAddresses{Addresses: addrs} + return k.ValidatorQueue.Set(ctx, collections.Join3(timeBzKeySize, endTime, uint64(endHeight)), valAddrs) } // InsertUnbondingValidatorQueue inserts a given unbonding validator address into @@ -447,8 +435,7 @@ func (k Keeper) InsertUnbondingValidatorQueue(ctx context.Context, val types.Val // DeleteValidatorQueueTimeSlice deletes all entries in the queue indexed by a // given height and time. func (k Keeper) DeleteValidatorQueueTimeSlice(ctx context.Context, endTime time.Time, endHeight int64) error { - store := k.storeService.OpenKVStore(ctx) - return store.Delete(types.GetValidatorQueueKey(endTime, endHeight)) + return k.ValidatorQueue.Remove(ctx, collections.Join3(timeBzKeySize, endTime, uint64(endHeight))) } // DeleteValidatorQueue removes a validator by address from the unbonding queue @@ -485,92 +472,85 @@ func (k Keeper) DeleteValidatorQueue(ctx context.Context, val types.Validator) e return k.SetUnbondingValidatorsQueue(ctx, val.UnbondingTime, val.UnbondingHeight, newAddrs) } -// ValidatorQueueIterator returns an interator ranging over validators that are -// unbonding whose unbonding completion occurs at the given height and time. -func (k Keeper) ValidatorQueueIterator(ctx context.Context, endTime time.Time, endHeight int64) (corestore.Iterator, error) { - store := k.storeService.OpenKVStore(ctx) - return store.Iterator(types.ValidatorQueueKey, storetypes.InclusiveEndBytes(types.GetValidatorQueueKey(endTime, endHeight))) -} - // UnbondAllMatureValidators unbonds all the mature unbonding validators that // have finished their unbonding period. func (k Keeper) UnbondAllMatureValidators(ctx context.Context) error { sdkCtx := sdk.UnwrapSDKContext(ctx) blockTime := sdkCtx.BlockTime() - blockHeight := sdkCtx.BlockHeight() - - // unbondingValIterator will contains all validator addresses indexed under - // the ValidatorQueueKey prefix. Note, the entire index key is composed as - // ValidatorQueueKey | timeBzLen (8-byte big endian) | timeBz | heightBz (8-byte big endian), - // so it may be possible that certain validator addresses that are iterated - // over are not ready to unbond, so an explicit check is required. - unbondingValIterator, err := k.ValidatorQueueIterator(ctx, blockTime, blockHeight) - if err != nil { - return err + blockHeight := uint64(sdkCtx.BlockHeight()) + + rng := new(collections.Range[collections.Triple[uint64, time.Time, uint64]]). + EndInclusive(collections.Join3(uint64(29), blockTime, blockHeight)) + + return k.ValidatorQueue.Walk(ctx, rng, func(key collections.Triple[uint64, time.Time, uint64], value types.ValAddresses) (stop bool, err error) { + return false, k.unbondMatureValidators(ctx, blockHeight, blockTime, key, value) + }) +} + +func (k Keeper) unbondMatureValidators( + ctx context.Context, + blockHeight uint64, + blockTime time.Time, + key collections.Triple[uint64, time.Time, uint64], + addrs types.ValAddresses, +) error { + keyTime, keyHeight := key.K2(), key.K3() + + // All addresses for the given key have the same unbonding height and time. + // We only unbond if the height and time are less than the current height + // and time. + if keyHeight > blockHeight || keyTime.After(blockTime) { + return nil } - defer unbondingValIterator.Close() - for ; unbondingValIterator.Valid(); unbondingValIterator.Next() { - key := unbondingValIterator.Key() - keyTime, keyHeight, err := types.ParseValidatorQueueKey(key) + // finalize unbonding + for _, valAddr := range addrs.Addresses { + addr, err := k.validatorAddressCodec.StringToBytes(valAddr) + if err != nil { + return err + } + val, err := k.GetValidator(ctx, addr) if err != nil { - return fmt.Errorf("failed to parse unbonding key: %w", err) + return errorsmod.Wrap(err, "validator in the unbonding queue was not found") + } + + if !val.IsUnbonding() { + return fmt.Errorf("unexpected validator in unbonding queue; status was not unbonding") + } + + // if the ref count is not zero, early exit. + if val.UnbondingOnHoldRefCount != 0 { + return nil } - // All addresses for the given key have the same unbonding height and time. - // We only unbond if the height and time are less than the current height - // and time. - if keyHeight <= blockHeight && (keyTime.Before(blockTime) || keyTime.Equal(blockTime)) { - addrs := types.ValAddresses{} - if err = k.cdc.Unmarshal(unbondingValIterator.Value(), &addrs); err != nil { + // otherwise do proper unbonding + for _, id := range val.UnbondingIds { + if err = k.DeleteUnbondingIndex(ctx, id); err != nil { return err } + } + + val, err = k.UnbondingToUnbonded(ctx, val) + if err != nil { + return err + } - for _, valAddr := range addrs.Addresses { - addr, err := k.validatorAddressCodec.StringToBytes(valAddr) - if err != nil { - return err - } - val, err := k.GetValidator(ctx, addr) - if err != nil { - return errorsmod.Wrap(err, "validator in the unbonding queue was not found") - } - - if !val.IsUnbonding() { - return fmt.Errorf("unexpected validator in unbonding queue; status was not unbonding") - } - - if val.UnbondingOnHoldRefCount == 0 { - for _, id := range val.UnbondingIds { - if err = k.DeleteUnbondingIndex(ctx, id); err != nil { - return err - } - } - - val, err = k.UnbondingToUnbonded(ctx, val) - if err != nil { - return err - } - - if val.GetDelegatorShares().IsZero() { - str, err := k.validatorAddressCodec.StringToBytes(val.GetOperator()) - if err != nil { - return err - } - if err = k.RemoveValidator(ctx, str); err != nil { - return err - } - } else { - // remove unbonding ids - val.UnbondingIds = []uint64{} - } - - // remove validator from queue - if err = k.DeleteValidatorQueue(ctx, val); err != nil { - return err - } - } + if val.GetDelegatorShares().IsZero() { + str, err := k.validatorAddressCodec.StringToBytes(val.GetOperator()) + if err != nil { + return err } + if err = k.RemoveValidator(ctx, str); err != nil { + return err + } + } else { + // remove unbonding ids + val.UnbondingIds = []uint64{} + } + + // remove validator from queue + if err = k.DeleteValidatorQueue(ctx, val); err != nil { + return err } } return nil diff --git a/x/staking/migrations/v2/store_test.go b/x/staking/migrations/v2/store_test.go index 10a6145365d1..bbd865f3cde1 100644 --- a/x/staking/migrations/v2/store_test.go +++ b/x/staking/migrations/v2/store_test.go @@ -111,7 +111,7 @@ func TestStoreMigration(t *testing.T) { { "ValidatorQueueKey", v1.GetValidatorQueueKey(now, 4), - types.GetValidatorQueueKey(now, 4), + getValidatorQueueKey(now, 4), }, { "HistoricalInfoKey", @@ -161,3 +161,26 @@ func getValidatorKey(operatorAddr sdk.ValAddress) []byte { func unbondingKey(delAddr sdk.AccAddress, valAddr sdk.ValAddress) []byte { return append(append(types.UnbondingDelegationKey, sdkaddress.MustLengthPrefix(delAddr)...), sdkaddress.MustLengthPrefix(valAddr)...) } + +func getValidatorQueueKey(timestamp time.Time, height int64) []byte { + heightBz := sdk.Uint64ToBigEndian(uint64(height)) + timeBz := sdk.FormatTimeBytes(timestamp) + timeBzL := len(timeBz) + prefixL := len(types.ValidatorQueueKey) + + bz := make([]byte, prefixL+8+timeBzL+8) + + // copy the prefix + copy(bz[:prefixL], types.ValidatorQueueKey) + + // copy the encoded time bytes length + copy(bz[prefixL:prefixL+8], sdk.Uint64ToBigEndian(uint64(timeBzL))) + + // copy the encoded time bytes + copy(bz[prefixL+8:prefixL+8+timeBzL], timeBz) + + // copy the encoded height + copy(bz[prefixL+8+timeBzL:], heightBz) + + return bz +} diff --git a/x/staking/types/keys.go b/x/staking/types/keys.go index 665393d8ccc1..600cf56bb11d 100644 --- a/x/staking/types/keys.go +++ b/x/staking/types/keys.go @@ -1,9 +1,7 @@ package types import ( - "bytes" "encoding/binary" - "fmt" "time" "cosmossdk.io/collections" @@ -53,7 +51,7 @@ var ( UnbondingQueueKey = collections.NewPrefix(65) // prefix for the timestamps in unbonding queue RedelegationQueueKey = []byte{0x42} // prefix for the timestamps in redelegations queue - ValidatorQueueKey = []byte{0x43} // prefix for the timestamps in validator queue + ValidatorQueueKey = collections.NewPrefix(67) // prefix for the timestamps in validator queue HistoricalInfoKey = collections.NewPrefix(80) // prefix for the historical info ValidatorUpdatesKey = collections.NewPrefix(97) // prefix for the end block validator updates key @@ -142,50 +140,6 @@ func ParseValidatorPowerRankKey(key []byte) (operAddr []byte) { return operAddr } -// GetValidatorQueueKey returns the prefix key used for getting a set of unbonding -// validators whose unbonding completion occurs at the given time and height. -func GetValidatorQueueKey(timestamp time.Time, height int64) []byte { - heightBz := sdk.Uint64ToBigEndian(uint64(height)) - timeBz := sdk.FormatTimeBytes(timestamp) - timeBzL := len(timeBz) - prefixL := len(ValidatorQueueKey) - - bz := make([]byte, prefixL+8+timeBzL+8) - - // copy the prefix - copy(bz[:prefixL], ValidatorQueueKey) - - // copy the encoded time bytes length - copy(bz[prefixL:prefixL+8], sdk.Uint64ToBigEndian(uint64(timeBzL))) - - // copy the encoded time bytes - copy(bz[prefixL+8:prefixL+8+timeBzL], timeBz) - - // copy the encoded height - copy(bz[prefixL+8+timeBzL:], heightBz) - - return bz -} - -// ParseValidatorQueueKey returns the encoded time and height from a key created -// from GetValidatorQueueKey. -func ParseValidatorQueueKey(bz []byte) (time.Time, int64, error) { - prefixL := len(ValidatorQueueKey) - if prefix := bz[:prefixL]; !bytes.Equal(prefix, ValidatorQueueKey) { - return time.Time{}, 0, fmt.Errorf("invalid prefix; expected: %X, got: %X", ValidatorQueueKey, prefix) - } - - timeBzL := sdk.BigEndianToUint64(bz[prefixL : prefixL+8]) - ts, err := sdk.ParseTimeBytes(bz[prefixL+8 : prefixL+8+int(timeBzL)]) - if err != nil { - return time.Time{}, 0, err - } - - height := sdk.BigEndianToUint64(bz[prefixL+8+int(timeBzL):]) - - return ts, int64(height), nil -} - // GetUBDKey creates the key for an unbonding delegation by delegator and validator addr // VALUE: staking/UnbondingDelegation func GetUBDKey(delAddr sdk.AccAddress, valAddr sdk.ValAddress) []byte { diff --git a/x/staking/types/keys_test.go b/x/staking/types/keys_test.go index e0ae6f66259e..93d381c28f11 100644 --- a/x/staking/types/keys_test.go +++ b/x/staking/types/keys_test.go @@ -1,11 +1,9 @@ package types_test import ( - "bytes" "encoding/hex" "math/big" "testing" - "time" "github.com/stretchr/testify/require" @@ -47,29 +45,3 @@ func TestGetValidatorPowerRank(t *testing.T) { require.Equal(t, tt.wantHex, got, "Keys did not match on test case %d", i) } } - -func TestGetValidatorQueueKey(t *testing.T) { - ts := time.Now() - height := int64(1024) - - bz := types.GetValidatorQueueKey(ts, height) - rTs, rHeight, err := types.ParseValidatorQueueKey(bz) - require.NoError(t, err) - require.Equal(t, ts.UTC(), rTs.UTC()) - require.Equal(t, rHeight, height) -} - -func TestTestGetValidatorQueueKeyOrder(t *testing.T) { - ts := time.Now().UTC() - height := int64(1000) - - endKey := types.GetValidatorQueueKey(ts, height) - - keyA := types.GetValidatorQueueKey(ts.Add(-10*time.Minute), height-10) - keyB := types.GetValidatorQueueKey(ts.Add(-5*time.Minute), height+50) - keyC := types.GetValidatorQueueKey(ts.Add(10*time.Minute), height+100) - - require.Equal(t, -1, bytes.Compare(keyA, endKey)) // keyA <= endKey - require.Equal(t, -1, bytes.Compare(keyB, endKey)) // keyB <= endKey - require.Equal(t, 1, bytes.Compare(keyC, endKey)) // keyB >= endKey -}