Add route for querying signing_info for all validators (#3952)

Also remove duplicate pagination:
- move function to extract query params into types/rest
- adjust pagination values locally until available in tendermint for validators

Code cleanup:
- helper function in test
- fix pagination description in swagger.yaml
- uint instead of int when possible

Closes: #3226
Closes: #3991
This commit is contained in:
Karoly Albert Szabo 2019-03-28 19:43:33 +01:00 committed by Alessio Treglia
parent e5897d8d91
commit 5bb6090e38
10 changed files with 321 additions and 90 deletions

View File

@ -0,0 +1 @@
#3949 added /slashing/signing_infos to get signing_info for all validators

View File

@ -785,6 +785,8 @@ func TestUnjail(t *testing.T) {
require.Equal(t, true, signingInfo.IndexOffset > 0)
require.Equal(t, time.Unix(0, 0).UTC(), signingInfo.JailedUntil)
require.Equal(t, true, signingInfo.MissedBlocksCounter == 0)
signingInfoList := getSigningInfoList(t, port)
require.NotZero(t, len(signingInfoList))
}
func TestProposalsQuery(t *testing.T) {

View File

@ -221,11 +221,11 @@ paths:
required: true
- in: query
name: page
description: Pagination page
description: Page number
type: integer
- in: query
name: size
description: Pagination size
name: limit
description: Maximum number of items per page
type: integer
responses:
200:
@ -885,22 +885,45 @@ paths:
200:
description: OK
schema:
type: object
properties:
start_height:
type: string
index_offset:
type: string
jailed_until:
type: string
missed_blocks_counter:
type: string
$ref: "#/definitions/SigningInfo"
204:
description: No sign info of this validator
400:
description: Invalid validator public key
500:
description: Internal Server Error
/slashing/signing_infos:
get:
summary: Get sign info of given all validators
description: Get sign info of all validators
produces:
- application/json
tags:
- ICS23
parameters:
- in: query
name: page
description: Page number
type: integer
required: true
- in: query
name: limit
description: Maximum number of items per page
type: integer
required: true
responses:
200:
description: OK
schema:
type: array
items:
$ref: "#/definitions/SigningInfo"
204:
description: No validators with sign info
400:
description: Invalid validator public key for one of the validators
500:
description: Internal Server Error
/slashing/validators/{validatorAddr}/unjail:
post:
summary: Unjail a jailed validator
@ -2181,3 +2204,14 @@ definitions:
type: array
items:
$ref: "#/definitions/Coin"
SigningInfo:
type: object
properties:
start_height:
type: string
index_offset:
type: string
jailed_until:
type: string
missed_blocks_counter:
type: string

View File

@ -1392,6 +1392,21 @@ func getSigningInfo(t *testing.T, port string, validatorPubKey string) slashing.
return signingInfo
}
// ----------------------------------------------------------------------
// ICS 23 - SlashingList
// ----------------------------------------------------------------------
// GET /slashing/signing_infos Get sign info of all validators with pagination
func getSigningInfoList(t *testing.T, port string) []slashing.ValidatorSigningInfo {
res, body := Request(t, port, "GET", "/slashing/signing_infos?page=1&limit=1", nil)
require.Equal(t, http.StatusOK, res.StatusCode, body)
var signingInfo []slashing.ValidatorSigningInfo
err := cdc.UnmarshalJSON([]byte(body), &signingInfo)
require.Nil(t, err)
return signingInfo
}
// TODO: Test this functionality, it is not currently in any of the tests
// POST /slashing/validators/{validatorAddr}/unjail Unjail a jailed validator
func doUnjail(

View File

@ -45,7 +45,7 @@ func ValidatorCommand(cdc *codec.Codec) *cobra.Command {
cliCtx := context.NewCLIContext().WithCodec(cdc)
result, err := getValidators(cliCtx, height)
result, err := GetValidators(cliCtx, height)
if err != nil {
return err
}
@ -113,7 +113,7 @@ func bech32ValidatorOutput(validator *tmtypes.Validator) (ValidatorOutput, error
}, nil
}
func getValidators(cliCtx context.CLIContext, height *int64) (ResultValidatorsOutput, error) {
func GetValidators(cliCtx context.CLIContext, height *int64) (ResultValidatorsOutput, error) {
// get the node
node, err := cliCtx.GetNode()
if err != nil {
@ -170,7 +170,7 @@ func ValidatorSetRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return
}
output, err := getValidators(cliCtx, &height)
output, err := GetValidators(cliCtx, &height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
@ -188,7 +188,7 @@ func LatestValidatorSetRequestHandlerFn(cliCtx context.CLIContext) http.HandlerF
return
}
output, err := getValidators(cliCtx, &height)
output, err := GetValidators(cliCtx, &height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return

View File

@ -4,15 +4,13 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
rest "github.com/cosmos/cosmos-sdk/types/rest"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -22,12 +20,10 @@ import (
)
const (
flagTags = "tags"
flagAny = "any"
flagPage = "page"
flagLimit = "limit"
defaultPage = 1
defaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19
flagTags = "tags"
flagAny = "any"
flagPage = "page"
flagLimit = "limit"
)
// default client command to search through tagged transactions
@ -96,8 +92,8 @@ $ gaiacli query txs --tags '<tag1>:<value1>&<tag2>:<value2>' --page 1 --limit 30
cmd.Flags().Bool(client.FlagTrustNode, false, "Trust connected full node (don't verify proofs for responses)")
viper.BindPFlag(client.FlagTrustNode, cmd.Flags().Lookup(client.FlagTrustNode))
cmd.Flags().String(flagTags, "", "tag:value list of tags that must match")
cmd.Flags().Int32(flagPage, defaultPage, "Query a specific page of paginated results")
cmd.Flags().Int32(flagLimit, defaultLimit, "Query number of transactions results per page returned")
cmd.Flags().Int32(flagPage, rest.DefaultPage, "Query a specific page of paginated results")
cmd.Flags().Int32(flagLimit, rest.DefaultLimit, "Query number of transactions results per page returned")
cmd.MarkFlagRequired(flagTags)
return cmd
}
@ -184,7 +180,7 @@ func SearchTxRequestHandlerFn(cliCtx context.CLIContext, cdc *codec.Codec) http.
return
}
tags, page, limit, err = parseHTTPArgs(r)
tags, page, limit, err = rest.ParseHTTPArgs(r)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
@ -200,51 +196,3 @@ func SearchTxRequestHandlerFn(cliCtx context.CLIContext, cdc *codec.Codec) http.
rest.PostProcessResponse(w, cdc, txs, cliCtx.Indent)
}
}
func parseHTTPArgs(r *http.Request) (tags []string, page, limit int, err error) {
tags = make([]string, 0, len(r.Form))
for key, values := range r.Form {
if key == "page" || key == "limit" {
continue
}
var value string
value, err = url.QueryUnescape(values[0])
if err != nil {
return tags, page, limit, err
}
var tag string
if key == types.TxHeightKey {
tag = fmt.Sprintf("%s=%s", key, value)
} else {
tag = fmt.Sprintf("%s='%s'", key, value)
}
tags = append(tags, tag)
}
pageStr := r.FormValue("page")
if pageStr == "" {
page = defaultPage
} else {
page, err = strconv.Atoi(pageStr)
if err != nil {
return tags, page, limit, err
} else if page <= 0 {
return tags, page, limit, errors.New("page must greater than 0")
}
}
limitStr := r.FormValue("limit")
if limitStr == "" {
limit = defaultLimit
} else {
limit, err = strconv.Atoi(limitStr)
if err != nil {
return tags, page, limit, err
} else if limit <= 0 {
return tags, page, limit, errors.New("limit must greater than 0")
}
}
return tags, page, limit, nil
}

View File

@ -3,9 +3,12 @@
package rest
import (
"errors"
"fmt"
"github.com/tendermint/tendermint/types"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
@ -13,6 +16,11 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
DefaultPage = 1
DefaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19
)
// GasEstimateResponse defines a response definition for tx gas estimation.
type GasEstimateResponse struct {
GasEstimate uint64 `json:"gas_estimate"`
@ -211,3 +219,53 @@ func PostProcessResponse(w http.ResponseWriter, cdc *codec.Codec, response inter
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(output)
}
// ParseHTTPArgs parses the request's URL and returns a slice containing all arguments pairs.
// It separates page and limit used for pagination
func ParseHTTPArgs(r *http.Request) (tags []string, page, limit int, err error) {
tags = make([]string, 0, len(r.Form))
for key, values := range r.Form {
if key == "page" || key == "limit" {
continue
}
var value string
value, err = url.QueryUnescape(values[0])
if err != nil {
return tags, page, limit, err
}
var tag string
if key == types.TxHeightKey {
tag = fmt.Sprintf("%s=%s", key, value)
} else {
tag = fmt.Sprintf("%s='%s'", key, value)
}
tags = append(tags, tag)
}
pageStr := r.FormValue("page")
if pageStr == "" {
page = DefaultPage
} else {
page, err = strconv.Atoi(pageStr)
if err != nil {
return tags, page, limit, err
} else if page <= 0 {
return tags, page, limit, errors.New("page must greater than 0")
}
}
limitStr := r.FormValue("limit")
if limitStr == "" {
limit = DefaultLimit
} else {
limit, err = strconv.Atoi(limitStr)
if err != nil {
return tags, page, limit, err
} else if limit <= 0 {
return tags, page, limit, errors.New("limit must greater than 0")
}
}
return tags, page, limit, nil
}

View File

@ -3,6 +3,7 @@
package rest
import (
"io"
"net/http"
"net/http/httptest"
"testing"
@ -55,3 +56,55 @@ func TestBaseReqValidateBasic(t *testing.T) {
})
}
}
func TestParseHTTPArgs(t *testing.T) {
req0 := mustNewRequest(t, "", "/", nil)
req1 := mustNewRequest(t, "", "/?limit=5", nil)
req2 := mustNewRequest(t, "", "/?page=5", nil)
req3 := mustNewRequest(t, "", "/?page=5&limit=5", nil)
reqE1 := mustNewRequest(t, "", "/?page=-1", nil)
reqE2 := mustNewRequest(t, "", "/?limit=-1", nil)
req4 := mustNewRequest(t, "", "/?foo=faa", nil)
tests := []struct {
name string
req *http.Request
w http.ResponseWriter
tags []string
page int
limit int
err bool
}{
{"no params", req0, httptest.NewRecorder(), []string{}, DefaultPage, DefaultLimit, false},
{"Limit", req1, httptest.NewRecorder(), []string{}, DefaultPage, 5, false},
{"Page", req2, httptest.NewRecorder(), []string{}, 5, DefaultLimit, false},
{"Page and limit", req3, httptest.NewRecorder(), []string{}, 5, 5, false},
{"error page 0", reqE1, httptest.NewRecorder(), []string{}, DefaultPage, DefaultLimit, true},
{"error limit 0", reqE2, httptest.NewRecorder(), []string{}, DefaultPage, DefaultLimit, true},
{"tags", req4, httptest.NewRecorder(), []string{"foo='faa'"}, DefaultPage, DefaultLimit, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tags, page, limit, err := ParseHTTPArgs(tt.req)
if tt.err {
require.NotNil(t, err)
} else {
require.Nil(t, err)
require.Equal(t, tt.tags, tags)
require.Equal(t, tt.page, page)
require.Equal(t, tt.limit, limit)
}
})
}
}
func mustNewRequest(t *testing.T, method, url string, body io.Reader) *http.Request {
req, err := http.NewRequest(method, url, body)
require.NoError(t, err)
err = req.ParseForm()
require.NoError(t, err)
return req
}

View File

@ -2,15 +2,14 @@ package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/rpc"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/slashing"
"github.com/gorilla/mux"
"net/http"
)
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec) {
@ -19,6 +18,11 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Co
signingInfoHandlerFn(cliCtx, slashing.StoreKey, cdc),
).Methods("GET")
r.HandleFunc(
"/slashing/signing_infos",
signingInfoHandlerListFn(cliCtx, slashing.StoreKey, cdc),
).Methods("GET").Queries("page", "{page}", "limit", "{limit}")
r.HandleFunc(
"/slashing/parameters",
queryParamsHandlerFn(cdc, cliCtx),
@ -26,39 +30,79 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Co
}
// http request handler to query signing info
// nolint: unparam
func signingInfoHandlerFn(cliCtx context.CLIContext, storeName string, cdc *codec.Codec) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
pk, err := sdk.GetConsPubKeyBech32(vars["validatorPubKey"])
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
key := slashing.GetValidatorSigningInfoKey(sdk.ConsAddress(pk.Address()))
signingInfo, code, err := getSigningInfo(cliCtx, storeName, cdc, pk.Address())
res, err := cliCtx.QueryStore(key, storeName)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
rest.WriteErrorResponse(w, code, err.Error())
return
}
if len(res) == 0 {
if code == http.StatusNoContent {
w.WriteHeader(http.StatusNoContent)
return
}
var signingInfo slashing.ValidatorSigningInfo
rest.PostProcessResponse(w, cdc, signingInfo, cliCtx.Indent)
}
}
err = cdc.UnmarshalBinaryLengthPrefixed(res, &signingInfo)
// http request handler to query signing info
func signingInfoHandlerListFn(cliCtx context.CLIContext, storeName string, cdc *codec.Codec) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var signingInfoList []slashing.ValidatorSigningInfo
_, page, limit, err := rest.ParseHTTPArgs(r)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
height, err := rpc.GetChainHeight(cliCtx)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cdc, signingInfo, cliCtx.Indent)
validators, err := rpc.GetValidators(cliCtx, &height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
if len(validators.Validators) == 0 {
w.WriteHeader(http.StatusNoContent)
return
}
// TODO: this should happen when querying Validators from RPC,
// as soon as it's available this is not needed anymore
// parameter page is (page-1) because ParseHTTPArgs starts with page 1, where our array start with 0
start, end := adjustPagination(uint(len(validators.Validators)), uint(page)-1, uint(limit))
for _, validator := range validators.Validators[start:end] {
address := validator.Address
signingInfo, code, err := getSigningInfo(cliCtx, storeName, cdc, address)
if err != nil {
rest.WriteErrorResponse(w, code, err.Error())
return
}
signingInfoList = append(signingInfoList, signingInfo)
}
if len(signingInfoList) == 0 {
w.WriteHeader(http.StatusNoContent)
return
}
rest.PostProcessResponse(w, cdc, signingInfoList, cliCtx.Indent)
}
}
@ -75,3 +119,48 @@ func queryParamsHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.Hand
rest.PostProcessResponse(w, cdc, res, cliCtx.Indent)
}
}
func getSigningInfo(cliCtx context.CLIContext, storeName string, cdc *codec.Codec, address []byte) (signingInfo slashing.ValidatorSigningInfo, code int, err error) {
key := slashing.GetValidatorSigningInfoKey(sdk.ConsAddress(address))
res, err := cliCtx.QueryStore(key, storeName)
if err != nil {
code = http.StatusInternalServerError
return
}
if len(res) == 0 {
code = http.StatusNoContent
return
}
err = cdc.UnmarshalBinaryLengthPrefixed(res, &signingInfo)
if err != nil {
code = http.StatusInternalServerError
return
}
return
}
// Adjust pagination with page starting from 0
func adjustPagination(size, page, limit uint) (start uint, end uint) {
// If someone asks for pages bigger than our dataset, just return everything
if limit > size {
return 0, size
}
// Do pagination when healthy, fallback to 0
start = 0
if page*limit < size {
start = page * limit
}
// Do pagination only when healthy, fallback to len(dataset)
end = size
if start+limit <= size {
end = start + limit
}
return start, end
}

View File

@ -0,0 +1,31 @@
package rest
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestAdjustPagination(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
size uint
page uint
limit uint
start uint
end uint
}{
{"Ok", 3, 0, 1, 0, 1},
{"Limit too big", 3, 1, 5, 0, 3},
{"Page over limit", 3, 2, 3, 0, 3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
start, end := adjustPagination(tt.size, tt.page, tt.limit)
require.Equal(t, tt.start, start)
require.Equal(t, tt.end, end)
})
}
}