From fca4cbef88284af662d79bb47aa34131b28b5485 Mon Sep 17 00:00:00 2001 From: Aditya Date: Wed, 18 Dec 2019 05:20:02 -0800 Subject: [PATCH] Merge PR #5380: ADR 17 Implementation: Historical Module --- CHANGELOG.md | 3 + x/staking/abci.go | 18 ++ x/staking/alias.go | 13 ++ x/staking/client/cli/query.go | 46 +++++ x/staking/client/rest/query.go | 37 ++++ x/staking/handler.go | 56 ------ x/staking/keeper/historical_info.go | 71 +++++++ x/staking/keeper/historical_info_test.go | 106 ++++++++++ x/staking/keeper/params.go | 8 + x/staking/keeper/querier.go | 23 +++ x/staking/keeper/querier_test.go | 189 ++++++++++++------ x/staking/keeper/val_state_change.go | 56 ++++++ x/staking/module.go | 4 +- x/staking/simulation/genesis.go | 2 +- x/staking/spec/01_state.md | 18 ++ x/staking/spec/04_begin_block.md | 16 ++ .../spec/{04_end_block.md => 05_end_block.md} | 2 +- x/staking/spec/{05_hooks.md => 06_hooks.md} | 2 +- x/staking/spec/{06_events.md => 07_events.md} | 2 +- x/staking/spec/07_params.md | 14 -- x/staking/spec/08_params.md | 15 ++ x/staking/spec/README.md | 19 +- x/staking/types/errors.go | 25 ++- x/staking/types/historical_info.go | 57 ++++++ x/staking/types/historical_info_test.go | 67 +++++++ x/staking/types/keys.go | 10 + x/staking/types/params.go | 56 ++++-- x/staking/types/querier.go | 12 ++ x/staking/types/validator.go | 23 +++ x/staking/types/validator_test.go | 32 +++ 30 files changed, 827 insertions(+), 175 deletions(-) create mode 100644 x/staking/abci.go create mode 100644 x/staking/keeper/historical_info.go create mode 100644 x/staking/keeper/historical_info_test.go create mode 100644 x/staking/spec/04_begin_block.md rename x/staking/spec/{04_end_block.md => 05_end_block.md} (99%) rename x/staking/spec/{05_hooks.md => 06_hooks.md} (99%) rename x/staking/spec/{06_events.md => 07_events.md} (99%) delete mode 100644 x/staking/spec/07_params.md create mode 100644 x/staking/spec/08_params.md create mode 100644 x/staking/types/historical_info.go create mode 100644 x/staking/types/historical_info_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 591b6e4c65d0..7516d35b4157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,6 +147,9 @@ that allows for arbitrary vesting periods. * `ValidateSigCountDecorator`: Validate the number of signatures in tx based on app-parameters. * `IncrementSequenceDecorator`: Increments the account sequence for each signer to prevent replay attacks. * (cli) [\#5223](https://github.com/cosmos/cosmos-sdk/issues/5223) Cosmos Ledger App v2.0.0 is now supported. The changes are backwards compatible and App v1.5.x is still supported. +* (x/staking) [\#5380](https://github.com/cosmos/cosmos-sdk/pull/5380) Introduced ability to store historical info entries in staking keeper, allows applications to introspect specified number of past headers and validator sets + * Introduces new parameter `HistoricalEntries` which allows applications to determine how many recent historical info entries they want to persist in store. Default value is 0. + * Introduces cli commands and rest routes to query historical information at a given height * (modules) [\#5249](https://github.com/cosmos/cosmos-sdk/pull/5249) Funds are now allowed to be directly sent to the community pool (via the distribution module account). * (keys) [\#4941](https://github.com/cosmos/cosmos-sdk/issues/4941) Introduce keybase option to allow overriding the default private key implementation of a key generated through the `keys add` cli command. diff --git a/x/staking/abci.go b/x/staking/abci.go new file mode 100644 index 000000000000..39755c24440c --- /dev/null +++ b/x/staking/abci.go @@ -0,0 +1,18 @@ +package staking + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking/keeper" + abci "github.com/tendermint/tendermint/abci/types" +) + +// BeginBlocker will persist the current header and validator set as a historical entry +// and prune the oldest entry based on the HistoricalEntries parameter +func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { + k.TrackHistoricalInfo(ctx) +} + +// Called every block, update validator set +func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate { + return k.BlockValidatorUpdates(ctx) +} diff --git a/x/staking/alias.go b/x/staking/alias.go index 019fa0298532..887b9b7bf690 100644 --- a/x/staking/alias.go +++ b/x/staking/alias.go @@ -19,6 +19,7 @@ const ( CodeInvalidDelegation = types.CodeInvalidDelegation CodeInvalidInput = types.CodeInvalidInput CodeValidatorJailed = types.CodeValidatorJailed + CodeInvalidHistoricalInfo = types.CodeInvalidHistoricalInfo CodeInvalidAddress = types.CodeInvalidAddress CodeUnauthorized = types.CodeUnauthorized CodeInternal = types.CodeInternal @@ -47,6 +48,7 @@ const ( QueryDelegatorValidator = types.QueryDelegatorValidator QueryPool = types.QueryPool QueryParameters = types.QueryParameters + QueryHistoricalInfo = types.QueryHistoricalInfo MaxMonikerLength = types.MaxMonikerLength MaxIdentityLength = types.MaxIdentityLength MaxWebsiteLength = types.MaxWebsiteLength @@ -86,6 +88,10 @@ var ( NewDelegationResp = types.NewDelegationResp NewRedelegationResponse = types.NewRedelegationResponse NewRedelegationEntryResponse = types.NewRedelegationEntryResponse + NewHistoricalInfo = types.NewHistoricalInfo + MustMarshalHistoricalInfo = types.MustMarshalHistoricalInfo + MustUnmarshalHistoricalInfo = types.MustUnmarshalHistoricalInfo + UnmarshalHistoricalInfo = types.UnmarshalHistoricalInfo ErrNilValidatorAddr = types.ErrNilValidatorAddr ErrBadValidatorAddr = types.ErrBadValidatorAddr ErrNoValidatorFound = types.ErrNoValidatorFound @@ -131,6 +137,8 @@ var ( ErrBothShareMsgsGiven = types.ErrBothShareMsgsGiven ErrNeitherShareMsgsGiven = types.ErrNeitherShareMsgsGiven ErrMissingSignature = types.ErrMissingSignature + ErrInvalidHistoricalInfo = types.ErrInvalidHistoricalInfo + ErrNoHistoricalInfo = types.ErrNoHistoricalInfo NewGenesisState = types.NewGenesisState DefaultGenesisState = types.DefaultGenesisState NewMultiStakingHooks = types.NewMultiStakingHooks @@ -159,6 +167,7 @@ var ( GetREDsFromValSrcIndexKey = types.GetREDsFromValSrcIndexKey GetREDsToValDstIndexKey = types.GetREDsToValDstIndexKey GetREDsByDelToValDstIndexKey = types.GetREDsByDelToValDstIndexKey + GetHistoricalInfoKey = types.GetHistoricalInfoKey NewMsgCreateValidator = types.NewMsgCreateValidator NewMsgEditValidator = types.NewMsgEditValidator NewMsgDelegate = types.NewMsgDelegate @@ -174,6 +183,7 @@ var ( NewQueryBondsParams = types.NewQueryBondsParams NewQueryRedelegationParams = types.NewQueryRedelegationParams NewQueryValidatorsParams = types.NewQueryValidatorsParams + NewQueryHistoricalInfoParams = types.NewQueryHistoricalInfoParams NewValidator = types.NewValidator MustMarshalValidator = types.MustMarshalValidator MustUnmarshalValidator = types.MustUnmarshalValidator @@ -196,6 +206,7 @@ var ( UnbondingQueueKey = types.UnbondingQueueKey RedelegationQueueKey = types.RedelegationQueueKey ValidatorQueueKey = types.ValidatorQueueKey + HistoricalInfoKey = types.HistoricalInfoKey KeyUnbondingTime = types.KeyUnbondingTime KeyMaxValidators = types.KeyMaxValidators KeyMaxEntries = types.KeyMaxEntries @@ -216,6 +227,7 @@ type ( Redelegation = types.Redelegation RedelegationEntry = types.RedelegationEntry Redelegations = types.Redelegations + HistoricalInfo = types.HistoricalInfo DelegationResponse = types.DelegationResponse DelegationResponses = types.DelegationResponses RedelegationResponse = types.RedelegationResponse @@ -237,6 +249,7 @@ type ( QueryBondsParams = types.QueryBondsParams QueryRedelegationParams = types.QueryRedelegationParams QueryValidatorsParams = types.QueryValidatorsParams + QueryHistoricalInfoParams = types.QueryHistoricalInfoParams Validator = types.Validator Validators = types.Validators Description = types.Description diff --git a/x/staking/client/cli/query.go b/x/staking/client/cli/query.go index ce7ca6e55be7..632237c1d915 100644 --- a/x/staking/client/cli/query.go +++ b/x/staking/client/cli/query.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "strconv" "strings" "github.com/spf13/cobra" @@ -35,6 +36,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { GetCmdQueryValidatorDelegations(queryRoute, cdc), GetCmdQueryValidatorUnbondingDelegations(queryRoute, cdc), GetCmdQueryValidatorRedelegations(queryRoute, cdc), + GetCmdQueryHistoricalInfo(queryRoute, cdc), GetCmdQueryParams(queryRoute, cdc), GetCmdQueryPool(queryRoute, cdc))...) @@ -527,6 +529,50 @@ $ %s query staking redelegation cosmos1gghjut3ccd8ay0zduzj64hwre2fxs9ld75ru9p } } +// GetCmdQueryHistoricalInfo implements the historical info query command +func GetCmdQueryHistoricalInfo(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "historical-info [height]", + Args: cobra.ExactArgs(1), + Short: "Query historical info at given height", + Long: strings.TrimSpace( + fmt.Sprintf(`Query historical info at given height. + +Example: +$ %s query staking historical-info 5 +`, + version.ClientName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + height, err := strconv.ParseInt(args[0], 10, 64) + if err != nil || height < 0 { + return fmt.Errorf("height argument provided must be a non-negative-integer: %v", err) + } + + bz, err := cdc.MarshalJSON(types.QueryHistoricalInfoParams{Height: height}) + if err != nil { + return err + } + + route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryHistoricalInfo) + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + return err + } + + var resp types.HistoricalInfo + if err := cdc.UnmarshalJSON(res, &resp); err != nil { + return err + } + + return cliCtx.PrintOutput(resp) + }, + } +} + // GetCmdQueryPool implements the pool query command. func GetCmdQueryPool(storeName string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ diff --git a/x/staking/client/rest/query.go b/x/staking/client/rest/query.go index e0eabd870e93..ddd6c03c7c6b 100644 --- a/x/staking/client/rest/query.go +++ b/x/staking/client/rest/query.go @@ -3,6 +3,7 @@ package rest import ( "fmt" "net/http" + "strconv" "strings" "github.com/gorilla/mux" @@ -86,6 +87,12 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { validatorUnbondingDelegationsHandlerFn(cliCtx), ).Methods("GET") + // Get HistoricalInfo at a given height + r.HandleFunc( + "/staking/historical_info/{height}", + historicalInfoHandlerFn(cliCtx), + ).Methods("GET") + // Get the current state of the staking pool r.HandleFunc( "/staking/pool", @@ -313,6 +320,36 @@ func validatorUnbondingDelegationsHandlerFn(cliCtx context.CLIContext) http.Hand return queryValidator(cliCtx, "custom/staking/validatorUnbondingDelegations") } +// HTTP request handler to query historical info at a given height +func historicalInfoHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + heightStr := vars["height"] + height, err := strconv.ParseInt(heightStr, 10, 64) + if err != nil || height < 0 { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("Must provide non-negative integer for height: %v", err)) + return + } + + params := types.NewQueryHistoricalInfoParams(height) + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryHistoricalInfo) + res, height, err := cliCtx.QueryWithData(route, bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + // HTTP request handler to query the pool information func poolHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { diff --git a/x/staking/handler.go b/x/staking/handler.go index 1d7ba1e84762..376da255ea1c 100644 --- a/x/staking/handler.go +++ b/x/staking/handler.go @@ -4,7 +4,6 @@ import ( "fmt" "time" - abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/common" tmtypes "github.com/tendermint/tendermint/types" @@ -40,61 +39,6 @@ func NewHandler(k keeper.Keeper) sdk.Handler { } } -// Called every block, update validator set -func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate { - // Calculate validator set changes. - // - // NOTE: ApplyAndReturnValidatorSetUpdates has to come before - // UnbondAllMatureValidatorQueue. - // This fixes a bug when the unbonding period is instant (is the case in - // some of the tests). The test expected the validator to be completely - // unbonded after the Endblocker (go from Bonded -> Unbonding during - // ApplyAndReturnValidatorSetUpdates and then Unbonding -> Unbonded during - // UnbondAllMatureValidatorQueue). - validatorUpdates := k.ApplyAndReturnValidatorSetUpdates(ctx) - - // Unbond all mature validators from the unbonding queue. - k.UnbondAllMatureValidatorQueue(ctx) - - // Remove all mature unbonding delegations from the ubd queue. - matureUnbonds := k.DequeueAllMatureUBDQueue(ctx, ctx.BlockHeader().Time) - for _, dvPair := range matureUnbonds { - err := k.CompleteUnbonding(ctx, dvPair.DelegatorAddress, dvPair.ValidatorAddress) - if err != nil { - continue - } - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeCompleteUnbonding, - sdk.NewAttribute(types.AttributeKeyValidator, dvPair.ValidatorAddress.String()), - sdk.NewAttribute(types.AttributeKeyDelegator, dvPair.DelegatorAddress.String()), - ), - ) - } - - // Remove all mature redelegations from the red queue. - matureRedelegations := k.DequeueAllMatureRedelegationQueue(ctx, ctx.BlockHeader().Time) - for _, dvvTriplet := range matureRedelegations { - err := k.CompleteRedelegation(ctx, dvvTriplet.DelegatorAddress, - dvvTriplet.ValidatorSrcAddress, dvvTriplet.ValidatorDstAddress) - if err != nil { - continue - } - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeCompleteRedelegation, - sdk.NewAttribute(types.AttributeKeyDelegator, dvvTriplet.DelegatorAddress.String()), - sdk.NewAttribute(types.AttributeKeySrcValidator, dvvTriplet.ValidatorSrcAddress.String()), - sdk.NewAttribute(types.AttributeKeyDstValidator, dvvTriplet.ValidatorDstAddress.String()), - ), - ) - } - - return validatorUpdates -} - // These functions assume everything has been authenticated, // now we just perform action and save diff --git a/x/staking/keeper/historical_info.go b/x/staking/keeper/historical_info.go new file mode 100644 index 000000000000..2ce0aee7dbcc --- /dev/null +++ b/x/staking/keeper/historical_info.go @@ -0,0 +1,71 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// GetHistoricalInfo gets the historical info at a given height +func (k Keeper) GetHistoricalInfo(ctx sdk.Context, height int64) (types.HistoricalInfo, bool) { + store := ctx.KVStore(k.storeKey) + key := types.GetHistoricalInfoKey(height) + + value := store.Get(key) + if value == nil { + return types.HistoricalInfo{}, false + } + + hi := types.MustUnmarshalHistoricalInfo(k.cdc, value) + return hi, true +} + +// SetHistoricalInfo sets the historical info at a given height +func (k Keeper) SetHistoricalInfo(ctx sdk.Context, height int64, hi types.HistoricalInfo) { + store := ctx.KVStore(k.storeKey) + key := types.GetHistoricalInfoKey(height) + + value := types.MustMarshalHistoricalInfo(k.cdc, hi) + store.Set(key, value) +} + +// DeleteHistoricalInfo deletes the historical info at a given height +func (k Keeper) DeleteHistoricalInfo(ctx sdk.Context, height int64) { + store := ctx.KVStore(k.storeKey) + key := types.GetHistoricalInfoKey(height) + + store.Delete(key) +} + +// TrackHistoricalInfo saves the latest historical-info and deletes the oldest +// heights that are below pruning height +func (k Keeper) TrackHistoricalInfo(ctx sdk.Context) { + entryNum := k.HistoricalEntries(ctx) + + // Prune store to ensure we only have parameter-defined historical entries. + // In most cases, this will involve removing a single historical entry. + // In the rare scenario when the historical entries gets reduced to a lower value k' + // from the original value k. k - k' entries must be deleted from the store. + // Since the entries to be deleted are always in a continuous range, we can iterate + // over the historical entries starting from the most recent version to be pruned + // and then return at the first empty entry. + for i := ctx.BlockHeight() - int64(entryNum); i >= 0; i-- { + _, found := k.GetHistoricalInfo(ctx, i) + if found { + k.DeleteHistoricalInfo(ctx, i) + } else { + break + } + } + + // if there is no need to persist historicalInfo, return + if entryNum == 0 { + return + } + + // Create HistoricalInfo struct + lastVals := k.GetLastValidators(ctx) + historicalEntry := types.NewHistoricalInfo(ctx.BlockHeader(), lastVals) + + // Set latest HistoricalInfo at current height + k.SetHistoricalInfo(ctx, ctx.BlockHeight(), historicalEntry) +} diff --git a/x/staking/keeper/historical_info_test.go b/x/staking/keeper/historical_info_test.go new file mode 100644 index 000000000000..fa1fa2356cd9 --- /dev/null +++ b/x/staking/keeper/historical_info_test.go @@ -0,0 +1,106 @@ +package keeper + +import ( + "sort" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking/types" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/stretchr/testify/require" +) + +func TestHistoricalInfo(t *testing.T) { + ctx, _, keeper, _ := CreateTestInput(t, false, 10) + validators := make([]types.Validator, len(addrVals)) + + for i, valAddr := range addrVals { + validators[i] = types.NewValidator(valAddr, PKs[i], types.Description{}) + } + + hi := types.NewHistoricalInfo(ctx.BlockHeader(), validators) + + keeper.SetHistoricalInfo(ctx, 2, hi) + + recv, found := keeper.GetHistoricalInfo(ctx, 2) + require.True(t, found, "HistoricalInfo not found after set") + require.Equal(t, hi, recv, "HistoricalInfo not equal") + require.True(t, sort.IsSorted(types.Validators(recv.ValSet)), "HistoricalInfo validators is not sorted") + + keeper.DeleteHistoricalInfo(ctx, 2) + + recv, found = keeper.GetHistoricalInfo(ctx, 2) + require.False(t, found, "HistoricalInfo found after delete") + require.Equal(t, types.HistoricalInfo{}, recv, "HistoricalInfo is not empty") +} + +func TestTrackHistoricalInfo(t *testing.T) { + ctx, _, k, _ := CreateTestInput(t, false, 10) + + // set historical entries in params to 5 + params := types.DefaultParams() + params.HistoricalEntries = 5 + k.SetParams(ctx, params) + + // set historical info at 5, 4 which should be pruned + // and check that it has been stored + h4 := abci.Header{ + ChainID: "HelloChain", + Height: 4, + } + h5 := abci.Header{ + ChainID: "HelloChain", + Height: 5, + } + valSet := []types.Validator{ + types.NewValidator(sdk.ValAddress(Addrs[0]), PKs[0], types.Description{}), + types.NewValidator(sdk.ValAddress(Addrs[1]), PKs[1], types.Description{}), + } + hi4 := types.NewHistoricalInfo(h4, valSet) + hi5 := types.NewHistoricalInfo(h5, valSet) + k.SetHistoricalInfo(ctx, 4, hi4) + k.SetHistoricalInfo(ctx, 5, hi5) + recv, found := k.GetHistoricalInfo(ctx, 4) + require.True(t, found) + require.Equal(t, hi4, recv) + recv, found = k.GetHistoricalInfo(ctx, 5) + require.True(t, found) + require.Equal(t, hi5, recv) + + // Set last validators in keeper + val1 := types.NewValidator(sdk.ValAddress(Addrs[2]), PKs[2], types.Description{}) + k.SetValidator(ctx, val1) + k.SetLastValidatorPower(ctx, val1.OperatorAddress, 10) + val2 := types.NewValidator(sdk.ValAddress(Addrs[3]), PKs[3], types.Description{}) + vals := []types.Validator{val1, val2} + sort.Sort(types.Validators(vals)) + k.SetValidator(ctx, val2) + k.SetLastValidatorPower(ctx, val2.OperatorAddress, 8) + + // Set Header for BeginBlock context + header := abci.Header{ + ChainID: "HelloChain", + Height: 10, + } + ctx = ctx.WithBlockHeader(header) + + k.TrackHistoricalInfo(ctx) + + // Check HistoricalInfo at height 10 is persisted + expected := types.HistoricalInfo{ + Header: header, + ValSet: vals, + } + recv, found = k.GetHistoricalInfo(ctx, 10) + require.True(t, found, "GetHistoricalInfo failed after BeginBlock") + require.Equal(t, expected, recv, "GetHistoricalInfo returned eunexpected result") + + // Check HistoricalInfo at height 5, 4 is pruned + recv, found = k.GetHistoricalInfo(ctx, 4) + require.False(t, found, "GetHistoricalInfo did not prune earlier height") + require.Equal(t, types.HistoricalInfo{}, recv, "GetHistoricalInfo at height 4 is not empty after prune") + recv, found = k.GetHistoricalInfo(ctx, 5) + require.False(t, found, "GetHistoricalInfo did not prune first prune height") + require.Equal(t, types.HistoricalInfo{}, recv, "GetHistoricalInfo at height 5 is not empty after prune") +} diff --git a/x/staking/keeper/params.go b/x/staking/keeper/params.go index dede56b2c19d..11f4b57bc55b 100644 --- a/x/staking/keeper/params.go +++ b/x/staking/keeper/params.go @@ -37,6 +37,13 @@ func (k Keeper) MaxEntries(ctx sdk.Context) (res uint16) { return } +// HistoricalEntries = number of historical info entries +// to persist in store +func (k Keeper) HistoricalEntries(ctx sdk.Context) (res uint16) { + k.paramstore.Get(ctx, types.KeyHistoricalEntries, &res) + return +} + // BondDenom - Bondable coin denomination func (k Keeper) BondDenom(ctx sdk.Context) (res string) { k.paramstore.Get(ctx, types.KeyBondDenom, &res) @@ -49,6 +56,7 @@ func (k Keeper) GetParams(ctx sdk.Context) types.Params { k.UnbondingTime(ctx), k.MaxValidators(ctx), k.MaxEntries(ctx), + k.HistoricalEntries(ctx), k.BondDenom(ctx), ) } diff --git a/x/staking/keeper/querier.go b/x/staking/keeper/querier.go index d97bb07b10ef..15bc59ee5ff6 100644 --- a/x/staking/keeper/querier.go +++ b/x/staking/keeper/querier.go @@ -38,6 +38,8 @@ func NewQuerier(k Keeper) sdk.Querier { return queryDelegatorValidators(ctx, req, k) case types.QueryDelegatorValidator: return queryDelegatorValidator(ctx, req, k) + case types.QueryHistoricalInfo: + return queryHistoricalInfo(ctx, req, k) case types.QueryPool: return queryPool(ctx, k) case types.QueryParameters: @@ -327,6 +329,27 @@ func queryRedelegations(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byt return res, nil } +func queryHistoricalInfo(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) { + var params types.QueryHistoricalInfoParams + + err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdk.ErrUnknownRequest(string(req.Data)) + } + + hi, found := k.GetHistoricalInfo(ctx, params.Height) + if !found { + return nil, types.ErrNoHistoricalInfo(types.DefaultCodespace) + } + + res, err := codec.MarshalJSONIndent(types.ModuleCdc, hi) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + + return res, nil +} + func queryPool(ctx sdk.Context, k Keeper) ([]byte, sdk.Error) { bondDenom := k.BondDenom(ctx) bondedPool := k.GetBondedPool(ctx) diff --git a/x/staking/keeper/querier_test.go b/x/staking/keeper/querier_test.go index 2dac9b6f99ab..320266ca1a28 100644 --- a/x/staking/keeper/querier_test.go +++ b/x/staking/keeper/querier_test.go @@ -31,6 +31,13 @@ func TestNewQuerier(t *testing.T) { keeper.SetValidatorByPowerIndex(ctx, validators[i]) } + header := abci.Header{ + ChainID: "HelloChain", + Height: 5, + } + hi := types.NewHistoricalInfo(header, validators[:]) + keeper.SetHistoricalInfo(ctx, 5, hi) + query := abci.RequestQuery{ Path: "", Data: []byte{}, @@ -39,53 +46,63 @@ func TestNewQuerier(t *testing.T) { querier := NewQuerier(keeper) bz, err := querier(ctx, []string{"other"}, query) - require.NotNil(t, err) + require.Error(t, err) require.Nil(t, bz) _, err = querier(ctx, []string{"pool"}, query) - require.Nil(t, err) + require.NoError(t, err) _, err = querier(ctx, []string{"parameters"}, query) - require.Nil(t, err) + require.NoError(t, err) queryValParams := types.NewQueryValidatorParams(addrVal1) bz, errRes := cdc.MarshalJSON(queryValParams) - require.Nil(t, errRes) + require.NoError(t, errRes) query.Path = "/custom/staking/validator" query.Data = bz _, err = querier(ctx, []string{"validator"}, query) - require.Nil(t, err) + require.NoError(t, err) _, err = querier(ctx, []string{"validatorDelegations"}, query) - require.Nil(t, err) + require.NoError(t, err) _, err = querier(ctx, []string{"validatorUnbondingDelegations"}, query) - require.Nil(t, err) + require.NoError(t, err) queryDelParams := types.NewQueryDelegatorParams(addrAcc2) bz, errRes = cdc.MarshalJSON(queryDelParams) - require.Nil(t, errRes) + require.NoError(t, errRes) query.Path = "/custom/staking/validator" query.Data = bz _, err = querier(ctx, []string{"delegatorDelegations"}, query) - require.Nil(t, err) + require.NoError(t, err) _, err = querier(ctx, []string{"delegatorUnbondingDelegations"}, query) - require.Nil(t, err) + require.NoError(t, err) _, err = querier(ctx, []string{"delegatorValidators"}, query) - require.Nil(t, err) + require.NoError(t, err) bz, errRes = cdc.MarshalJSON(types.NewQueryRedelegationParams(nil, nil, nil)) - require.Nil(t, errRes) + require.NoError(t, errRes) query.Data = bz _, err = querier(ctx, []string{"redelegations"}, query) - require.Nil(t, err) + require.NoError(t, err) + + queryHisParams := types.NewQueryHistoricalInfoParams(5) + bz, errRes = cdc.MarshalJSON(queryHisParams) + require.NoError(t, errRes) + + query.Path = "/custom/staking/historicalInfo" + query.Data = bz + + _, err = querier(ctx, []string{"historicalInfo"}, query) + require.NoError(t, err) } func TestQueryParametersPool(t *testing.T) { @@ -94,21 +111,21 @@ func TestQueryParametersPool(t *testing.T) { bondDenom := sdk.DefaultBondDenom res, err := queryParameters(ctx, keeper) - require.Nil(t, err) + require.NoError(t, err) var params types.Params errRes := cdc.UnmarshalJSON(res, ¶ms) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Equal(t, keeper.GetParams(ctx), params) res, err = queryPool(ctx, keeper) - require.Nil(t, err) + require.NoError(t, err) var pool types.Pool bondedPool := keeper.GetBondedPool(ctx) notBondedPool := keeper.GetNotBondedPool(ctx) errRes = cdc.UnmarshalJSON(res, &pool) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Equal(t, bondedPool.GetCoins().AmountOf(bondDenom), pool.BondedTokens) require.Equal(t, notBondedPool.GetCoins().AmountOf(bondDenom), pool.NotBondedTokens) } @@ -138,7 +155,7 @@ func TestQueryValidators(t *testing.T) { for i, s := range status { queryValsParams := types.NewQueryValidatorsParams(1, int(params.MaxValidators), s.String()) bz, err := cdc.MarshalJSON(queryValsParams) - require.Nil(t, err) + require.NoError(t, err) req := abci.RequestQuery{ Path: fmt.Sprintf("/custom/%s/%s", types.QuerierRoute, types.QueryValidators), @@ -146,11 +163,11 @@ func TestQueryValidators(t *testing.T) { } res, err := queryValidators(ctx, req, keeper) - require.Nil(t, err) + require.NoError(t, err) var validatorsResp []types.Validator err = cdc.UnmarshalJSON(res, &validatorsResp) - require.Nil(t, err) + require.NoError(t, err) require.Equal(t, 1, len(validatorsResp)) require.ElementsMatch(t, validators[i].OperatorAddress, validatorsResp[0].OperatorAddress) @@ -160,18 +177,18 @@ func TestQueryValidators(t *testing.T) { // Query each validator queryParams := types.NewQueryValidatorParams(addrVal1) bz, err := cdc.MarshalJSON(queryParams) - require.Nil(t, err) + require.NoError(t, err) query := abci.RequestQuery{ Path: "/custom/staking/validator", Data: bz, } res, err := queryValidator(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) var validator types.Validator err = cdc.UnmarshalJSON(res, &validator) - require.Nil(t, err) + require.NoError(t, err) require.Equal(t, queriedValidators[0], validator) } @@ -199,7 +216,7 @@ func TestQueryDelegation(t *testing.T) { // Query Delegator bonded validators queryParams := types.NewQueryDelegatorParams(addrAcc2) bz, errRes := cdc.MarshalJSON(queryParams) - require.Nil(t, errRes) + require.NoError(t, errRes) query := abci.RequestQuery{ Path: "/custom/staking/delegatorValidators", @@ -209,11 +226,11 @@ func TestQueryDelegation(t *testing.T) { delValidators := keeper.GetDelegatorValidators(ctx, addrAcc2, params.MaxValidators) res, err := queryDelegatorValidators(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) var validatorsResp []types.Validator errRes = cdc.UnmarshalJSON(res, &validatorsResp) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Equal(t, len(delValidators), len(validatorsResp)) require.ElementsMatch(t, delValidators, validatorsResp) @@ -222,12 +239,12 @@ func TestQueryDelegation(t *testing.T) { query.Data = bz[:len(bz)-1] _, err = queryDelegatorValidators(ctx, query, keeper) - require.NotNil(t, err) + require.Error(t, err) // Query bonded validator queryBondParams := types.NewQueryBondsParams(addrAcc2, addrVal1) bz, errRes = cdc.MarshalJSON(queryBondParams) - require.Nil(t, errRes) + require.NoError(t, errRes) query = abci.RequestQuery{ Path: "/custom/staking/delegatorValidator", @@ -235,11 +252,11 @@ func TestQueryDelegation(t *testing.T) { } res, err = queryDelegatorValidator(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) var validator types.Validator errRes = cdc.UnmarshalJSON(res, &validator) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Equal(t, delValidators[0], validator) @@ -247,7 +264,7 @@ func TestQueryDelegation(t *testing.T) { query.Data = bz[:len(bz)-1] _, err = queryDelegatorValidator(ctx, query, keeper) - require.NotNil(t, err) + require.Error(t, err) // Query delegation @@ -260,11 +277,11 @@ func TestQueryDelegation(t *testing.T) { require.True(t, found) res, err = queryDelegation(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) var delegationRes types.DelegationResponse errRes = cdc.UnmarshalJSON(res, &delegationRes) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Equal(t, delegation.ValidatorAddress, delegationRes.ValidatorAddress) require.Equal(t, delegation.DelegatorAddress, delegationRes.DelegatorAddress) @@ -277,11 +294,11 @@ func TestQueryDelegation(t *testing.T) { } res, err = queryDelegatorDelegations(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) var delegatorDelegations types.DelegationResponses errRes = cdc.UnmarshalJSON(res, &delegatorDelegations) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Len(t, delegatorDelegations, 1) require.Equal(t, delegation.ValidatorAddress, delegatorDelegations[0].ValidatorAddress) require.Equal(t, delegation.DelegatorAddress, delegatorDelegations[0].DelegatorAddress) @@ -291,12 +308,12 @@ func TestQueryDelegation(t *testing.T) { query.Data = bz[:len(bz)-1] _, err = queryDelegation(ctx, query, keeper) - require.NotNil(t, err) + require.Error(t, err) // Query validator delegations bz, errRes = cdc.MarshalJSON(types.NewQueryValidatorParams(addrVal1)) - require.Nil(t, errRes) + require.NoError(t, errRes) query = abci.RequestQuery{ Path: "custom/staking/validatorDelegations", @@ -304,11 +321,11 @@ func TestQueryDelegation(t *testing.T) { } res, err = queryValidatorDelegations(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) var delegationsRes types.DelegationResponses errRes = cdc.UnmarshalJSON(res, &delegationsRes) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Len(t, delegatorDelegations, 1) require.Equal(t, delegation.ValidatorAddress, delegationsRes[0].ValidatorAddress) require.Equal(t, delegation.DelegatorAddress, delegationsRes[0].DelegatorAddress) @@ -317,11 +334,11 @@ func TestQueryDelegation(t *testing.T) { // Query unbonging delegation unbondingTokens := sdk.TokensFromConsensusPower(10) _, err = keeper.Undelegate(ctx, addrAcc2, val1.OperatorAddress, unbondingTokens.ToDec()) - require.Nil(t, err) + require.NoError(t, err) queryBondParams = types.NewQueryBondsParams(addrAcc2, addrVal1) bz, errRes = cdc.MarshalJSON(queryBondParams) - require.Nil(t, errRes) + require.NoError(t, errRes) query = abci.RequestQuery{ Path: "/custom/staking/unbondingDelegation", @@ -332,11 +349,11 @@ func TestQueryDelegation(t *testing.T) { require.True(t, found) res, err = queryUnbondingDelegation(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) var unbondRes types.UnbondingDelegation errRes = cdc.UnmarshalJSON(res, &unbondRes) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Equal(t, unbond, unbondRes) @@ -344,7 +361,7 @@ func TestQueryDelegation(t *testing.T) { query.Data = bz[:len(bz)-1] _, err = queryUnbondingDelegation(ctx, query, keeper) - require.NotNil(t, err) + require.Error(t, err) // Query Delegator Delegations @@ -354,29 +371,29 @@ func TestQueryDelegation(t *testing.T) { } res, err = queryDelegatorUnbondingDelegations(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) var delegatorUbds []types.UnbondingDelegation errRes = cdc.UnmarshalJSON(res, &delegatorUbds) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Equal(t, unbond, delegatorUbds[0]) // error unknown request query.Data = bz[:len(bz)-1] _, err = queryDelegatorUnbondingDelegations(ctx, query, keeper) - require.NotNil(t, err) + require.Error(t, err) // Query redelegation redelegationTokens := sdk.TokensFromConsensusPower(10) _, err = keeper.BeginRedelegation(ctx, addrAcc2, val1.OperatorAddress, val2.OperatorAddress, redelegationTokens.ToDec()) - require.Nil(t, err) + require.NoError(t, err) redel, found := keeper.GetRedelegation(ctx, addrAcc2, val1.OperatorAddress, val2.OperatorAddress) require.True(t, found) bz, errRes = cdc.MarshalJSON(types.NewQueryRedelegationParams(addrAcc2, val1.OperatorAddress, val2.OperatorAddress)) - require.Nil(t, errRes) + require.NoError(t, errRes) query = abci.RequestQuery{ Path: "/custom/staking/redelegations", @@ -384,11 +401,11 @@ func TestQueryDelegation(t *testing.T) { } res, err = queryRedelegations(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) var redelRes types.RedelegationResponses errRes = cdc.UnmarshalJSON(res, &redelRes) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Len(t, redelRes, 1) require.Equal(t, redel.DelegatorAddress, redelRes[0].DelegatorAddress) require.Equal(t, redel.ValidatorSrcAddress, redelRes[0].ValidatorSrcAddress) @@ -420,7 +437,7 @@ func TestQueryRedelegations(t *testing.T) { // delegator redelegations queryDelegatorParams := types.NewQueryDelegatorParams(addrAcc2) bz, errRes := cdc.MarshalJSON(queryDelegatorParams) - require.Nil(t, errRes) + require.NoError(t, errRes) query := abci.RequestQuery{ Path: "/custom/staking/redelegations", @@ -428,11 +445,11 @@ func TestQueryRedelegations(t *testing.T) { } res, err := queryRedelegations(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) var redelRes types.RedelegationResponses errRes = cdc.UnmarshalJSON(res, &redelRes) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Len(t, redelRes, 1) require.Equal(t, redel.DelegatorAddress, redelRes[0].DelegatorAddress) require.Equal(t, redel.ValidatorSrcAddress, redelRes[0].ValidatorSrcAddress) @@ -442,7 +459,7 @@ func TestQueryRedelegations(t *testing.T) { // validator redelegations queryValidatorParams := types.NewQueryValidatorParams(val1.GetOperator()) bz, errRes = cdc.MarshalJSON(queryValidatorParams) - require.Nil(t, errRes) + require.NoError(t, errRes) query = abci.RequestQuery{ Path: "/custom/staking/redelegations", @@ -450,10 +467,10 @@ func TestQueryRedelegations(t *testing.T) { } res, err = queryRedelegations(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) errRes = cdc.UnmarshalJSON(res, &redelRes) - require.Nil(t, errRes) + require.NoError(t, errRes) require.Len(t, redelRes, 1) require.Equal(t, redel.DelegatorAddress, redelRes[0].DelegatorAddress) require.Equal(t, redel.ValidatorSrcAddress, redelRes[0].ValidatorSrcAddress) @@ -489,13 +506,13 @@ func TestQueryUnbondingDelegation(t *testing.T) { // queryValidatorParams := types.NewQueryBondsParams(addrAcc1, val1.GetOperator()) bz, errRes := cdc.MarshalJSON(queryValidatorParams) - require.Nil(t, errRes) + require.NoError(t, errRes) query := abci.RequestQuery{ Path: "/custom/staking/unbondingDelegation", Data: bz, } res, err := queryUnbondingDelegation(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) require.NotNil(t, res) var ubDel types.UnbondingDelegation require.NoError(t, cdc.UnmarshalJSON(res, &ubDel)) @@ -508,26 +525,26 @@ func TestQueryUnbondingDelegation(t *testing.T) { // queryValidatorParams = types.NewQueryBondsParams(addrAcc2, val1.GetOperator()) bz, errRes = cdc.MarshalJSON(queryValidatorParams) - require.Nil(t, errRes) + require.NoError(t, errRes) query = abci.RequestQuery{ Path: "/custom/staking/unbondingDelegation", Data: bz, } _, err = queryUnbondingDelegation(ctx, query, keeper) - require.NotNil(t, err) + require.Error(t, err) // // found: query unbonding delegation by delegator and validator // queryDelegatorParams := types.NewQueryDelegatorParams(addrAcc1) bz, errRes = cdc.MarshalJSON(queryDelegatorParams) - require.Nil(t, errRes) + require.NoError(t, errRes) query = abci.RequestQuery{ Path: "/custom/staking/delegatorUnbondingDelegations", Data: bz, } res, err = queryDelegatorUnbondingDelegations(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) require.NotNil(t, res) var ubDels []types.UnbondingDelegation require.NoError(t, cdc.UnmarshalJSON(res, &ubDels)) @@ -540,14 +557,56 @@ func TestQueryUnbondingDelegation(t *testing.T) { // queryDelegatorParams = types.NewQueryDelegatorParams(addrAcc2) bz, errRes = cdc.MarshalJSON(queryDelegatorParams) - require.Nil(t, errRes) + require.NoError(t, errRes) query = abci.RequestQuery{ Path: "/custom/staking/delegatorUnbondingDelegations", Data: bz, } res, err = queryDelegatorUnbondingDelegations(ctx, query, keeper) - require.Nil(t, err) + require.NoError(t, err) require.NotNil(t, res) require.NoError(t, cdc.UnmarshalJSON(res, &ubDels)) require.Equal(t, 0, len(ubDels)) } + +func TestQueryHistoricalInfo(t *testing.T) { + cdc := codec.New() + ctx, _, keeper, _ := CreateTestInput(t, false, 10000) + + // Create Validators and Delegation + val1 := types.NewValidator(addrVal1, pk1, types.Description{}) + val2 := types.NewValidator(addrVal2, pk2, types.Description{}) + vals := []types.Validator{val1, val2} + keeper.SetValidator(ctx, val1) + keeper.SetValidator(ctx, val2) + + header := abci.Header{ + ChainID: "HelloChain", + Height: 5, + } + hi := types.NewHistoricalInfo(header, vals) + keeper.SetHistoricalInfo(ctx, 5, hi) + + queryHistoricalParams := types.NewQueryHistoricalInfoParams(4) + bz, errRes := cdc.MarshalJSON(queryHistoricalParams) + require.NoError(t, errRes) + query := abci.RequestQuery{ + Path: "/custom/staking/historicalInfo", + Data: bz, + } + res, err := queryHistoricalInfo(ctx, query, keeper) + require.Error(t, err, "Invalid query passed") + require.Nil(t, res, "Invalid query returned non-nil result") + + queryHistoricalParams = types.NewQueryHistoricalInfoParams(5) + bz, errRes = cdc.MarshalJSON(queryHistoricalParams) + require.NoError(t, errRes) + query.Data = bz + res, err = queryHistoricalInfo(ctx, query, keeper) + require.NoError(t, err, "Valid query passed") + require.NotNil(t, res, "Valid query returned nil result") + + var recv types.HistoricalInfo + require.NoError(t, cdc.UnmarshalJSON(res, &recv)) + require.Equal(t, hi, recv, "HistoricalInfo query returned wrong result") +} diff --git a/x/staking/keeper/val_state_change.go b/x/staking/keeper/val_state_change.go index 724ebaff0590..f43a8f9da5a7 100644 --- a/x/staking/keeper/val_state_change.go +++ b/x/staking/keeper/val_state_change.go @@ -11,6 +11,62 @@ import ( "github.com/cosmos/cosmos-sdk/x/staking/types" ) +// Calculate the ValidatorUpdates for the current block +// Called in each EndBlock +func (k Keeper) BlockValidatorUpdates(ctx sdk.Context) []abci.ValidatorUpdate { + // Calculate validator set changes. + // + // NOTE: ApplyAndReturnValidatorSetUpdates has to come before + // UnbondAllMatureValidatorQueue. + // This fixes a bug when the unbonding period is instant (is the case in + // some of the tests). The test expected the validator to be completely + // unbonded after the Endblocker (go from Bonded -> Unbonding during + // ApplyAndReturnValidatorSetUpdates and then Unbonding -> Unbonded during + // UnbondAllMatureValidatorQueue). + validatorUpdates := k.ApplyAndReturnValidatorSetUpdates(ctx) + + // Unbond all mature validators from the unbonding queue. + k.UnbondAllMatureValidatorQueue(ctx) + + // Remove all mature unbonding delegations from the ubd queue. + matureUnbonds := k.DequeueAllMatureUBDQueue(ctx, ctx.BlockHeader().Time) + for _, dvPair := range matureUnbonds { + err := k.CompleteUnbonding(ctx, dvPair.DelegatorAddress, dvPair.ValidatorAddress) + if err != nil { + continue + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCompleteUnbonding, + sdk.NewAttribute(types.AttributeKeyValidator, dvPair.ValidatorAddress.String()), + sdk.NewAttribute(types.AttributeKeyDelegator, dvPair.DelegatorAddress.String()), + ), + ) + } + + // Remove all mature redelegations from the red queue. + matureRedelegations := k.DequeueAllMatureRedelegationQueue(ctx, ctx.BlockHeader().Time) + for _, dvvTriplet := range matureRedelegations { + err := k.CompleteRedelegation(ctx, dvvTriplet.DelegatorAddress, + dvvTriplet.ValidatorSrcAddress, dvvTriplet.ValidatorDstAddress) + if err != nil { + continue + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCompleteRedelegation, + sdk.NewAttribute(types.AttributeKeyDelegator, dvvTriplet.DelegatorAddress.String()), + sdk.NewAttribute(types.AttributeKeySrcValidator, dvvTriplet.ValidatorSrcAddress.String()), + sdk.NewAttribute(types.AttributeKeyDstValidator, dvvTriplet.ValidatorDstAddress.String()), + ), + ) + } + + return validatorUpdates +} + // Apply and return accumulated updates to the bonded validator set. Also, // * Updates the active valset as keyed by LastValidatorPowerKey. // * Updates the total power as keyed by LastTotalPowerKey. diff --git a/x/staking/module.go b/x/staking/module.go index 45a06e67244a..69a8cb940504 100644 --- a/x/staking/module.go +++ b/x/staking/module.go @@ -165,7 +165,9 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { } // BeginBlock returns the begin blocker for the staking module. -func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} +func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { + BeginBlocker(ctx, am.keeper) +} // EndBlock returns the end blocker for the staking module. It returns no validator // updates. diff --git a/x/staking/simulation/genesis.go b/x/staking/simulation/genesis.go index d0455386298c..cca02bee8cc9 100644 --- a/x/staking/simulation/genesis.go +++ b/x/staking/simulation/genesis.go @@ -50,7 +50,7 @@ func RandomizedGenState(simState *module.SimulationState) { // NewSimulationManager constructor for this to work simState.UnbondTime = unbondTime - params := types.NewParams(simState.UnbondTime, maxValidators, 7, sdk.DefaultBondDenom) + params := types.NewParams(simState.UnbondTime, maxValidators, 7, 3, sdk.DefaultBondDenom) // validators & delegations var ( diff --git a/x/staking/spec/01_state.md b/x/staking/spec/01_state.md index bf388a143615..11b63d8dfc32 100644 --- a/x/staking/spec/01_state.md +++ b/x/staking/spec/01_state.md @@ -282,3 +282,21 @@ The stored object as each key is an array of validator operator addresses from which the validator object can be accessed. Typically it is expected that only a single validator record will be associated with a given timestamp however it is possible that multiple validators exist in the queue at the same location. + +## HistoricalInfo + +HistoricalInfo objects are stored and pruned at each block such that the staking keeper persists +the `n` most recent historical info defined by staking module parameter: `HistoricalEntries`. + +```go +type HistoricalInfo struct { + Header abci.Header + ValSet []types.Validator +} +``` + +At each BeginBlock, the staking keeper will persist the current Header and the Validators that committed +the current block in a `HistoricalInfo` object. The Validators are sorted on their address to ensure that +they are in a determisnistic order. +The oldest HistoricalEntries will be pruned to ensure that there only exist the parameter-defined number of +historical entries. diff --git a/x/staking/spec/04_begin_block.md b/x/staking/spec/04_begin_block.md new file mode 100644 index 000000000000..3ba615b5de4a --- /dev/null +++ b/x/staking/spec/04_begin_block.md @@ -0,0 +1,16 @@ + + +# Begin-Block + +Each abci begin block call, the historical info will get stored and pruned +according to the `HistoricalEntries` parameter. + +## Historical Info Tracking + +If the `HistoricalEntries` parameter is 0, then the `BeginBlock` performs a no-op. + +Otherwise, the latest historical info is stored under the key `historicalInfoKey|height`, while any entries older than `height - HistoricalEntries` is deleted. +In most cases, this results in a single entry being pruned per block. +However, if the parameter `HistoricalEntries` has changed to a lower value there will be multiple entries in the store that must be pruned. diff --git a/x/staking/spec/04_end_block.md b/x/staking/spec/05_end_block.md similarity index 99% rename from x/staking/spec/04_end_block.md rename to x/staking/spec/05_end_block.md index 3207c078cd30..16444134f424 100644 --- a/x/staking/spec/04_end_block.md +++ b/x/staking/spec/05_end_block.md @@ -1,5 +1,5 @@ # End-Block diff --git a/x/staking/spec/05_hooks.md b/x/staking/spec/06_hooks.md similarity index 99% rename from x/staking/spec/05_hooks.md rename to x/staking/spec/06_hooks.md index 2eac2d4a1708..c2c372b624d8 100644 --- a/x/staking/spec/05_hooks.md +++ b/x/staking/spec/06_hooks.md @@ -1,5 +1,5 @@ # Hooks diff --git a/x/staking/spec/06_events.md b/x/staking/spec/07_events.md similarity index 99% rename from x/staking/spec/06_events.md rename to x/staking/spec/07_events.md index 48162355cf97..e16cf009570d 100644 --- a/x/staking/spec/06_events.md +++ b/x/staking/spec/07_events.md @@ -1,5 +1,5 @@ # Events diff --git a/x/staking/spec/07_params.md b/x/staking/spec/07_params.md deleted file mode 100644 index 6df38579dc81..000000000000 --- a/x/staking/spec/07_params.md +++ /dev/null @@ -1,14 +0,0 @@ - - -# Parameters - -The staking module contains the following parameters: - -| Key | Type | Example | -|---------------|------------------|-------------------| -| UnbondingTime | string (time ns) | "259200000000000" | -| MaxValidators | uint16 | 100 | -| KeyMaxEntries | uint16 | 7 | -| BondDenom | string | "uatom" | diff --git a/x/staking/spec/08_params.md b/x/staking/spec/08_params.md new file mode 100644 index 000000000000..3d87879e18a0 --- /dev/null +++ b/x/staking/spec/08_params.md @@ -0,0 +1,15 @@ + + +# Parameters + +The staking module contains the following parameters: + +| Key | Type | Example | +|-------------------|------------------|-------------------| +| UnbondingTime | string (time ns) | "259200000000000" | +| MaxValidators | uint16 | 100 | +| KeyMaxEntries | uint16 | 7 | +| HistoricalEntries | uint16 | 3 | +| BondDenom | string | "uatom" | diff --git a/x/staking/spec/README.md b/x/staking/spec/README.md index 7f61b900cd92..437790ce74e2 100644 --- a/x/staking/spec/README.md +++ b/x/staking/spec/README.md @@ -32,6 +32,7 @@ network. - [UnbondingDelegation](01_state.md#unbondingdelegation) - [Redelegation](01_state.md#redelegation) - [Queues](01_state.md#queues) + - [HistoricalInfo](01_state.md#historicalinfo) 2. **[State Transitions](02_state_transitions.md)** - [Validators](02_state_transitions.md#validators) - [Delegations](02_state_transitions.md#delegations) @@ -42,11 +43,13 @@ network. - [MsgDelegate](03_messages.md#msgdelegate) - [MsgBeginUnbonding](03_messages.md#msgbeginunbonding) - [MsgBeginRedelegate](03_messages.md#msgbeginredelegate) -4. **[End-Block ](04_end_block.md)** - - [Validator Set Changes](04_end_block.md#validator-set-changes) - - [Queues ](04_end_block.md#queues-) -5. **[Hooks](05_hooks.md)** -6. **[Events](06_events.md)** - - [EndBlocker](06_events.md#endblocker) - - [Handlers](06_events.md#handlers) -7. **[Parameters](07_params.md)** +4. **[Begin-Block](04_begin_block.md)** + - [Historical Info Tracking](04_begin_block.md#historical-info-tracking) +4. **[End-Block ](05_end_block.md)** + - [Validator Set Changes](05_end_block.md#validator-set-changes) + - [Queues ](05_end_block.md#queues-) +5. **[Hooks](06_hooks.md)** +6. **[Events](07_events.md)** + - [EndBlocker](07_events.md#endblocker) + - [Handlers](07_events.md#handlers) +7. **[Parameters](08_params.md)** diff --git a/x/staking/types/errors.go b/x/staking/types/errors.go index 012f9421f8ee..523e720e2d98 100644 --- a/x/staking/types/errors.go +++ b/x/staking/types/errors.go @@ -14,14 +14,15 @@ type CodeType = sdk.CodeType const ( DefaultCodespace sdk.CodespaceType = ModuleName - CodeInvalidValidator CodeType = 101 - CodeInvalidDelegation CodeType = 102 - CodeInvalidInput CodeType = 103 - CodeValidatorJailed CodeType = 104 - CodeInvalidAddress CodeType = sdk.CodeInvalidAddress - CodeUnauthorized CodeType = sdk.CodeUnauthorized - CodeInternal CodeType = sdk.CodeInternal - CodeUnknownRequest CodeType = sdk.CodeUnknownRequest + CodeInvalidValidator CodeType = 101 + CodeInvalidDelegation CodeType = 102 + CodeInvalidInput CodeType = 103 + CodeValidatorJailed CodeType = 104 + CodeInvalidHistoricalInfo CodeType = 105 + CodeInvalidAddress CodeType = sdk.CodeInvalidAddress + CodeUnauthorized CodeType = sdk.CodeUnauthorized + CodeInternal CodeType = sdk.CodeInternal + CodeUnknownRequest CodeType = sdk.CodeUnknownRequest ) //validator @@ -212,3 +213,11 @@ func ErrNeitherShareMsgsGiven(codespace sdk.CodespaceType) sdk.Error { func ErrMissingSignature(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidValidator, "missing signature") } + +func ErrInvalidHistoricalInfo(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidHistoricalInfo, "invalid historical info") +} + +func ErrNoHistoricalInfo(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidHistoricalInfo, "no historical info found") +} diff --git a/x/staking/types/historical_info.go b/x/staking/types/historical_info.go new file mode 100644 index 000000000000..8fae88f07f4e --- /dev/null +++ b/x/staking/types/historical_info.go @@ -0,0 +1,57 @@ +package types + +import ( + "sort" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/codec" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// HistoricalInfo contains the historical information that gets stored at each height +type HistoricalInfo struct { + Header abci.Header `json:"header" yaml:"header"` + ValSet []Validator `json:"valset" yaml:"valset"` +} + +// NewHistoricalInfo will create a historical information struct from header and valset +// it will first sort valset before inclusion into historical info +func NewHistoricalInfo(header abci.Header, valSet []Validator) HistoricalInfo { + sort.Sort(Validators(valSet)) + return HistoricalInfo{ + Header: header, + ValSet: valSet, + } +} + +// MustMarshalHistoricalInfo wll marshal historical info and panic on error +func MustMarshalHistoricalInfo(cdc *codec.Codec, hi HistoricalInfo) []byte { + return cdc.MustMarshalBinaryLengthPrefixed(hi) +} + +// MustUnmarshalHistoricalInfo wll unmarshal historical info and panic on error +func MustUnmarshalHistoricalInfo(cdc *codec.Codec, value []byte) HistoricalInfo { + hi, err := UnmarshalHistoricalInfo(cdc, value) + if err != nil { + panic(err) + } + return hi +} + +// UnmarshalHistoricalInfo will unmarshal historical info and return any error +func UnmarshalHistoricalInfo(cdc *codec.Codec, value []byte) (hi HistoricalInfo, err error) { + err = cdc.UnmarshalBinaryLengthPrefixed(value, &hi) + return hi, err +} + +// ValidateBasic will ensure HistoricalInfo is not nil and sorted +func ValidateBasic(hi HistoricalInfo) error { + if len(hi.ValSet) == 0 { + return sdkerrors.Wrap(ErrInvalidHistoricalInfo(DefaultCodespace), "ValidatorSer is nil") + } + if !sort.IsSorted(Validators(hi.ValSet)) { + return sdkerrors.Wrap(ErrInvalidHistoricalInfo(DefaultCodespace), "ValidatorSet is not sorted by address") + } + return nil +} diff --git a/x/staking/types/historical_info_test.go b/x/staking/types/historical_info_test.go new file mode 100644 index 000000000000..f607ba15031c --- /dev/null +++ b/x/staking/types/historical_info_test.go @@ -0,0 +1,67 @@ +package types + +import ( + "math/rand" + "sort" + "testing" + + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" +) + +var ( + validators = []Validator{ + NewValidator(valAddr1, pk1, Description{}), + NewValidator(valAddr2, pk2, Description{}), + NewValidator(valAddr3, pk3, Description{}), + } + header = abci.Header{ + ChainID: "hello", + Height: 5, + } +) + +func TestHistoricalInfo(t *testing.T) { + hi := NewHistoricalInfo(header, validators) + require.True(t, sort.IsSorted(Validators(hi.ValSet)), "Validators are not sorted") + + var value []byte + require.NotPanics(t, func() { + value = MustMarshalHistoricalInfo(ModuleCdc, hi) + }) + + require.NotNil(t, value, "Marshalled HistoricalInfo is nil") + + recv, err := UnmarshalHistoricalInfo(ModuleCdc, value) + require.Nil(t, err, "Unmarshalling HistoricalInfo failed") + require.Equal(t, hi, recv, "Unmarshalled HistoricalInfo is different from original") + require.True(t, sort.IsSorted(Validators(hi.ValSet)), "Validators are not sorted") +} + +func TestValidateBasic(t *testing.T) { + hi := HistoricalInfo{ + Header: header, + } + err := ValidateBasic(hi) + require.Error(t, err, "ValidateBasic passed on nil ValSet") + + // Ensure validators are not sorted + for sort.IsSorted(Validators(validators)) { + rand.Shuffle(len(validators), func(i, j int) { + it := validators[i] + validators[i] = validators[j] + validators[j] = it + }) + } + + hi = HistoricalInfo{ + Header: header, + ValSet: validators, + } + err = ValidateBasic(hi) + require.Error(t, err, "ValidateBasic passed on unsorted ValSet") + + hi = NewHistoricalInfo(header, validators) + err = ValidateBasic(hi) + require.NoError(t, err, "ValidateBasic failed on valid HistoricalInfo") +} diff --git a/x/staking/types/keys.go b/x/staking/types/keys.go index 372b0b924294..d278b50db3e7 100644 --- a/x/staking/types/keys.go +++ b/x/staking/types/keys.go @@ -2,6 +2,7 @@ package types import ( "encoding/binary" + "strconv" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -45,6 +46,8 @@ var ( UnbondingQueueKey = []byte{0x41} // 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 + + HistoricalInfoKey = []byte{0x50} // prefix for the historical info ) // gets the key for the validator with address @@ -278,3 +281,10 @@ func GetREDsByDelToValDstIndexKey(delAddr sdk.AccAddress, valDstAddr sdk.ValAddr GetREDsToValDstIndexKey(valDstAddr), delAddr.Bytes()...) } + +//________________________________________________________________________________ + +// GetHistoricalInfoKey gets the key for the historical info +func GetHistoricalInfoKey(height int64) []byte { + return append(HistoricalInfoKey, []byte(strconv.FormatInt(height, 10))...) +} diff --git a/x/staking/types/params.go b/x/staking/types/params.go index 00d7aed07fb7..f3aefb404899 100644 --- a/x/staking/types/params.go +++ b/x/staking/types/params.go @@ -24,35 +24,42 @@ const ( // Default maximum entries in a UBD/RED pair DefaultMaxEntries uint16 = 7 + + // DefaultHistorical entries is 0 since it must only be non-zero for + // IBC connected chains + DefaultHistoricalEntries uint16 = 0 ) // nolint - Keys for parameter access var ( - KeyUnbondingTime = []byte("UnbondingTime") - KeyMaxValidators = []byte("MaxValidators") - KeyMaxEntries = []byte("KeyMaxEntries") - KeyBondDenom = []byte("BondDenom") + KeyUnbondingTime = []byte("UnbondingTime") + KeyMaxValidators = []byte("MaxValidators") + KeyMaxEntries = []byte("KeyMaxEntries") + KeyBondDenom = []byte("BondDenom") + KeyHistoricalEntries = []byte("HistoricalEntries") ) var _ params.ParamSet = (*Params)(nil) // Params defines the high level settings for staking type Params struct { - UnbondingTime time.Duration `json:"unbonding_time" yaml:"unbonding_time"` // time duration of unbonding - MaxValidators uint16 `json:"max_validators" yaml:"max_validators"` // maximum number of validators (max uint16 = 65535) - MaxEntries uint16 `json:"max_entries" yaml:"max_entries"` // max entries for either unbonding delegation or redelegation (per pair/trio) - BondDenom string `json:"bond_denom" yaml:"bond_denom"` // bondable coin denomination + UnbondingTime time.Duration `json:"unbonding_time" yaml:"unbonding_time"` // time duration of unbonding + MaxValidators uint16 `json:"max_validators" yaml:"max_validators"` // maximum number of validators (max uint16 = 65535) + MaxEntries uint16 `json:"max_entries" yaml:"max_entries"` // max entries for either unbonding delegation or redelegation (per pair/trio) + HistoricalEntries uint16 `json:"historical_entries" yaml:"historical_entries"` // number of historical entries to persist + BondDenom string `json:"bond_denom" yaml:"bond_denom"` // bondable coin denomination } // NewParams creates a new Params instance -func NewParams(unbondingTime time.Duration, maxValidators, maxEntries uint16, +func NewParams(unbondingTime time.Duration, maxValidators, maxEntries, historicalEntries uint16, bondDenom string) Params { return Params{ - UnbondingTime: unbondingTime, - MaxValidators: maxValidators, - MaxEntries: maxEntries, - BondDenom: bondDenom, + UnbondingTime: unbondingTime, + MaxValidators: maxValidators, + MaxEntries: maxEntries, + HistoricalEntries: historicalEntries, + BondDenom: bondDenom, } } @@ -62,6 +69,7 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs { params.NewParamSetPair(KeyUnbondingTime, &p.UnbondingTime, validateUnbondingTime), params.NewParamSetPair(KeyMaxValidators, &p.MaxValidators, validateMaxValidators), params.NewParamSetPair(KeyMaxEntries, &p.MaxEntries, validateMaxEntries), + params.NewParamSetPair(KeyHistoricalEntries, &p.HistoricalEntries, validateHistoricalEntries), params.NewParamSetPair(KeyBondDenom, &p.BondDenom, validateBondDenom), } } @@ -76,17 +84,18 @@ func (p Params) Equal(p2 Params) bool { // DefaultParams returns a default set of parameters. func DefaultParams() Params { - return NewParams(DefaultUnbondingTime, DefaultMaxValidators, DefaultMaxEntries, sdk.DefaultBondDenom) + return NewParams(DefaultUnbondingTime, DefaultMaxValidators, DefaultMaxEntries, DefaultHistoricalEntries, sdk.DefaultBondDenom) } // String returns a human readable string representation of the parameters. func (p Params) String() string { return fmt.Sprintf(`Params: - Unbonding Time: %s - Max Validators: %d - Max Entries: %d - Bonded Coin Denom: %s`, p.UnbondingTime, - p.MaxValidators, p.MaxEntries, p.BondDenom) + Unbonding Time: %s + Max Validators: %d + Max Entries: %d + Historical Entries: %d + Bonded Coin Denom: %s`, p.UnbondingTime, + p.MaxValidators, p.MaxEntries, p.HistoricalEntries, p.BondDenom) } // unmarshal the current staking params value from store key or panic @@ -164,6 +173,15 @@ func validateMaxEntries(i interface{}) error { return nil } +func validateHistoricalEntries(i interface{}) error { + _, ok := i.(uint16) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + return nil +} + func validateBondDenom(i interface{}) error { v, ok := i.(string) if !ok { diff --git a/x/staking/types/querier.go b/x/staking/types/querier.go index ba38a55742f5..adc387354ea9 100644 --- a/x/staking/types/querier.go +++ b/x/staking/types/querier.go @@ -20,6 +20,7 @@ const ( QueryDelegatorValidator = "delegatorValidator" QueryPool = "pool" QueryParameters = "parameters" + QueryHistoricalInfo = "historicalInfo" ) // defines the params for the following queries: @@ -96,3 +97,14 @@ type QueryValidatorsParams struct { func NewQueryValidatorsParams(page, limit int, status string) QueryValidatorsParams { return QueryValidatorsParams{page, limit, status} } + +// QueryHistoricalInfoParams defines the params for the following queries: +// - 'custom/staking/historicalInfo' +type QueryHistoricalInfoParams struct { + Height int64 +} + +// NewQueryHistoricalInfoParams creates a new QueryHistoricalInfoParams instance +func NewQueryHistoricalInfoParams(height int64) QueryHistoricalInfoParams { + return QueryHistoricalInfoParams{height} +} diff --git a/x/staking/types/validator.go b/x/staking/types/validator.go index df68c177e930..37e6cbc02575 100644 --- a/x/staking/types/validator.go +++ b/x/staking/types/validator.go @@ -3,6 +3,7 @@ package types import ( "bytes" "fmt" + "sort" "strings" "time" @@ -102,6 +103,28 @@ func (v Validators) ToSDKValidators() (validators []exported.ValidatorI) { return validators } +// Sort Validators sorts validator array in ascending operator address order +func (v Validators) Sort() { + sort.Sort(v) +} + +// Implements sort interface +func (v Validators) Len() int { + return len(v) +} + +// Implements sort interface +func (v Validators) Less(i, j int) bool { + return bytes.Compare(v[i].OperatorAddress, v[j].OperatorAddress) == -1 +} + +// Implements sort interface +func (v Validators) Swap(i, j int) { + it := v[i] + v[i] = v[j] + v[j] = it +} + // NewValidator - initialize a new validator func NewValidator(operator sdk.ValAddress, pubKey crypto.PubKey, description Description) Validator { return Validator{ diff --git a/x/staking/types/validator_test.go b/x/staking/types/validator_test.go index 6f8243efab4b..f705d0d8df99 100644 --- a/x/staking/types/validator_test.go +++ b/x/staking/types/validator_test.go @@ -2,10 +2,14 @@ package types import ( "fmt" + "math/rand" + "reflect" + "sort" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/ed25519" tmtypes "github.com/tendermint/tendermint/types" yaml "gopkg.in/yaml.v2" @@ -303,3 +307,31 @@ func TestValidatorMarshalYAML(t *testing.T) { `, validator.OperatorAddress.String(), bechifiedPub) require.Equal(t, want, string(bs)) } + +// Check that sort will create deterministic ordering of validators +func TestValidatorsSortDeterminism(t *testing.T) { + vals := make([]Validator, 10) + sortedVals := make([]Validator, 10) + + // Create random validator slice + for i := range vals { + pk := ed25519.GenPrivKey().PubKey() + vals[i] = NewValidator(sdk.ValAddress(pk.Address()), pk, Description{}) + } + + // Save sorted copy + sort.Sort(Validators(vals)) + copy(sortedVals, vals) + + // Randomly shuffle validators, sort, and check it is equal to original sort + for i := 0; i < 10; i++ { + rand.Shuffle(10, func(i, j int) { + it := vals[i] + vals[i] = vals[j] + vals[j] = it + }) + + Validators(vals).Sort() + require.True(t, reflect.DeepEqual(sortedVals, vals), "Validator sort returned different slices") + } +}