diff --git a/.pending/features/gaiarest/3949-added-slashing-validators-signing_info-to-get-signing_info-for-all-validators b/.pending/features/gaiarest/3949-added-slashing-validators-signing_info-to-get-signing_info-for-all-validators new file mode 100644 index 0000000000..048f24cda0 --- /dev/null +++ b/.pending/features/gaiarest/3949-added-slashing-validators-signing_info-to-get-signing_info-for-all-validators @@ -0,0 +1 @@ +#3949 added /slashing/signing_infos to get signing_info for all validators \ No newline at end of file diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 4d93dab343..f17ac7776b 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -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) { diff --git a/client/lcd/swagger-ui/swagger.yaml b/client/lcd/swagger-ui/swagger.yaml index f2e90f0ef0..0ec9ce34a7 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -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 diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index 0828df5528..d9a4a0e7e4 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -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( diff --git a/client/rpc/validators.go b/client/rpc/validators.go index 2e5b0cb73a..0c81f81516 100644 --- a/client/rpc/validators.go +++ b/client/rpc/validators.go @@ -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 diff --git a/client/tx/search.go b/client/tx/search.go index 718ab5d030..3ee0206768 100644 --- a/client/tx/search.go +++ b/client/tx/search.go @@ -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 ':&:' --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 -} diff --git a/types/rest/rest.go b/types/rest/rest.go index d24e8e3d70..20fcef3fc7 100644 --- a/types/rest/rest.go +++ b/types/rest/rest.go @@ -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 +} diff --git a/types/rest/rest_test.go b/types/rest/rest_test.go index 876cffe683..e0acbd5b4b 100644 --- a/types/rest/rest_test.go +++ b/types/rest/rest_test.go @@ -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 +} diff --git a/x/slashing/client/rest/query.go b/x/slashing/client/rest/query.go index 2f037e5fba..3c4afdd00e 100644 --- a/x/slashing/client/rest/query.go +++ b/x/slashing/client/rest/query.go @@ -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 +} diff --git a/x/slashing/client/rest/query_test.go b/x/slashing/client/rest/query_test.go new file mode 100644 index 0000000000..ad177c23b5 --- /dev/null +++ b/x/slashing/client/rest/query_test.go @@ -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) + }) + } +}