From 951d939cdb89c08c1237d521dcbe3b4564725524 Mon Sep 17 00:00:00 2001
From: Silas Davis <silas@monax.io>
Date: Tue, 26 Jun 2018 14:43:20 +0100
Subject: [PATCH] Add last_block_info endpoint for healthcheck Also cleans up
 rpc/lib argument parsing

Signed-off-by: Silas Davis <silas@monax.io>
---
 rpc/lib/rpc_test.go             |  8 ++---
 rpc/lib/server/handlers.go      | 49 ++++++++++++++++---------------
 rpc/lib/server/handlers_test.go |  7 ++---
 rpc/lib/server/http_server.go   |  2 +-
 rpc/lib/types/error_codes.go    | 52 +++++++++++++++++++++++++++++++++
 rpc/lib/types/types.go          | 29 +++++++++++-------
 rpc/lib/types/types_test.go     |  4 +--
 rpc/result.go                   |  8 +++++
 rpc/result_test.go              | 17 +++++++++++
 rpc/service.go                  | 34 +++++++++++++++++++++
 rpc/tm/methods.go               |  4 +++
 11 files changed, 169 insertions(+), 45 deletions(-)
 create mode 100644 rpc/lib/types/error_codes.go

diff --git a/rpc/lib/rpc_test.go b/rpc/lib/rpc_test.go
index 99941c88..1d21c21a 100644
--- a/rpc/lib/rpc_test.go
+++ b/rpc/lib/rpc_test.go
@@ -187,22 +187,22 @@ func testWithHTTPClient(t *testing.T, cl client.HTTPClient) {
 	val := "acbd"
 	got, err := echoViaHTTP(cl, val)
 	require.Nil(t, err)
-	assert.Equal(t, got, val)
+	assert.Equal(t, val, got)
 
 	val2 := randBytes(t)
 	got2, err := echoBytesViaHTTP(cl, val2)
 	require.Nil(t, err)
-	assert.Equal(t, got2, val2)
+	assert.Equal(t, val2, got2)
 
 	val3 := cmn.HexBytes(randBytes(t))
 	got3, err := echoDataBytesViaHTTP(cl, val3)
 	require.Nil(t, err)
-	assert.Equal(t, got3, val3)
+	assert.Equal(t, val3, got3)
 
 	val4 := rand.Intn(10000)
 	got4, err := echoIntViaHTTP(cl, val4)
 	require.Nil(t, err)
-	assert.Equal(t, got4, val4)
+	assert.Equal(t, val4, got4)
 }
 
 func echoViaWS(cl *client.WSClient, val string) (string, error) {
diff --git a/rpc/lib/server/handlers.go b/rpc/lib/server/handlers.go
index 31892c0f..506425b7 100644
--- a/rpc/lib/server/handlers.go
+++ b/rpc/lib/server/handlers.go
@@ -311,37 +311,40 @@ func jsonStringToArg(ty reflect.Type, arg string) (reflect.Value, error) {
 }
 
 func nonJSONToArg(ty reflect.Type, arg string) (reflect.Value, error, bool) {
-	isQuotedString := strings.HasPrefix(arg, `"`) && strings.HasSuffix(arg, `"`)
-	isHexString := strings.HasPrefix(strings.ToLower(arg), "0x")
 	expectingString := ty.Kind() == reflect.String
-	expectingByteSlice := ty.Kind() == reflect.Slice && ty.Elem().Kind() == reflect.Uint8
+	expectingBytes := (ty.Kind() == reflect.Slice || ty.Kind() == reflect.Array) && ty.Elem().Kind() == reflect.Uint8
 
-	if isHexString {
-		if !expectingString && !expectingByteSlice {
-			err := errors.Errorf("Got a hex string arg, but expected '%s'",
-				ty.Kind().String())
-			return reflect.ValueOf(nil), err, false
-		}
+	isQuotedString := strings.HasPrefix(arg, `"`) && strings.HasSuffix(arg, `"`)
 
-		var value []byte
-		value, err := hex.DecodeString(arg[2:])
-		if err != nil {
-			return reflect.ValueOf(nil), err, false
-		}
-		if ty.Kind() == reflect.String {
-			return reflect.ValueOf(string(value)), nil, true
-		}
-		return reflect.ValueOf([]byte(value)), nil, true
+	// Throw quoted strings at JSON parser later... because it always has...
+	if expectingString && !isQuotedString {
+		return reflect.ValueOf(arg), nil, true
 	}
 
-	if isQuotedString && expectingByteSlice {
-		v := reflect.New(reflect.TypeOf(""))
-		err := json.Unmarshal([]byte(arg), v.Interface())
+	if expectingBytes {
+		if isQuotedString {
+			rv := reflect.New(ty)
+			err := json.Unmarshal([]byte(arg), rv.Interface())
+			if err != nil {
+				return reflect.ValueOf(nil), err, false
+			}
+			return rv.Elem(), nil, true
+		}
+		if strings.HasPrefix(strings.ToLower(arg), "0x") {
+			arg = arg[2:]
+		}
+		var value []byte
+		value, err := hex.DecodeString(arg)
 		if err != nil {
 			return reflect.ValueOf(nil), err, false
 		}
-		v = v.Elem()
-		return reflect.ValueOf([]byte(v.String())), nil, true
+		if ty.Kind() == reflect.Array {
+			// Gives us an empty array of the right type
+			rv := reflect.New(ty).Elem()
+			reflect.Copy(rv, reflect.ValueOf(value))
+			return rv, nil, true
+		}
+		return reflect.ValueOf(value), nil, true
 	}
 
 	return reflect.ValueOf(nil), nil, false
diff --git a/rpc/lib/server/handlers_test.go b/rpc/lib/server/handlers_test.go
index be66d56c..1c7ce385 100644
--- a/rpc/lib/server/handlers_test.go
+++ b/rpc/lib/server/handlers_test.go
@@ -37,8 +37,8 @@ func TestRPCParams(t *testing.T) {
 		wantErr string
 	}{
 		// bad
-		{`{"jsonrpc": "2.0", "id": "0"}`, "Method not found"},
-		{`{"jsonrpc": "2.0", "method": "y", "id": "0"}`, "Method not found"},
+		{`{"jsonrpc": "2.0", "id": "0"}`, "Method Not Found"},
+		{`{"jsonrpc": "2.0", "method": "y", "id": "0"}`, "Method Not Found"},
 		{`{"method": "c", "id": "0", "params": a}`, "invalid character"},
 		{`{"method": "c", "id": "0", "params": ["a"]}`, "got 1"},
 		{`{"method": "c", "id": "0", "params": ["a", "b"]}`, "of type int"},
@@ -56,7 +56,6 @@ func TestRPCParams(t *testing.T) {
 		mux.ServeHTTP(rec, req)
 		res := rec.Result()
 		// Always expecting back a JSONRPCResponse
-		assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i)
 		blob, err := ioutil.ReadAll(res.Body)
 		if err != nil {
 			t.Errorf("#%d: err reading body: %v", i, err)
@@ -66,7 +65,7 @@ func TestRPCParams(t *testing.T) {
 		recv := new(types.RPCResponse)
 		assert.Nil(t, json.Unmarshal(blob, recv), "#%d: expecting successful parsing of an RPCResponse:\nblob: %s", i, blob)
 		assert.NotEqual(t, recv, new(types.RPCResponse), "#%d: not expecting a blank RPCResponse", i)
-
+		assert.Equal(t, recv.Error.HTTPStatusCode(), res.StatusCode, "#%d: status should match error code", i)
 		if tt.wantErr == "" {
 			assert.Nil(t, recv.Error, "#%d: not expecting an error", i)
 		} else {
diff --git a/rpc/lib/server/http_server.go b/rpc/lib/server/http_server.go
index e0734304..e080ff26 100644
--- a/rpc/lib/server/http_server.go
+++ b/rpc/lib/server/http_server.go
@@ -46,7 +46,7 @@ func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) {
 		panic(err)
 	}
 	w.Header().Set("Content-Type", "application/json")
-	w.WriteHeader(200)
+	w.WriteHeader(res.Error.HTTPStatusCode())
 	w.Write(jsonBytes) // nolint: errcheck, gas
 }
 
diff --git a/rpc/lib/types/error_codes.go b/rpc/lib/types/error_codes.go
new file mode 100644
index 00000000..4ef91dd2
--- /dev/null
+++ b/rpc/lib/types/error_codes.go
@@ -0,0 +1,52 @@
+package types
+
+import (
+	"net/http"
+	"strconv"
+)
+
+// From JSONRPC 2.0 spec
+type RPCErrorCode int
+
+const (
+	RPCErrorCodeParseError     RPCErrorCode = -32700
+	RPCErrorCodeInvalidRequest RPCErrorCode = -32600
+	RPCErrorCodeMethodNotFound RPCErrorCode = -32601
+	RPCErrorCodeInvalidParams  RPCErrorCode = -32602
+	RPCErrorCodeInternalError  RPCErrorCode = -32603
+	RPCErrorCodeServerError    RPCErrorCode = -32000
+)
+
+func (code RPCErrorCode) String() string {
+	switch code {
+	case RPCErrorCodeParseError:
+		return "Parse Error"
+	case RPCErrorCodeInvalidRequest:
+		return "Parse Error"
+	case RPCErrorCodeMethodNotFound:
+		return "Method Not Found"
+	case RPCErrorCodeInvalidParams:
+		return "Invalid Params"
+	case RPCErrorCodeInternalError:
+		return "Internal Error"
+	case RPCErrorCodeServerError:
+		return "Server Error"
+	default:
+		return strconv.FormatInt(int64(code), 10)
+	}
+}
+
+func (code RPCErrorCode) HTTPStatusCode() int {
+	switch code {
+	case RPCErrorCodeInvalidRequest:
+		return http.StatusBadRequest
+	case RPCErrorCodeMethodNotFound:
+		return http.StatusMethodNotAllowed
+	default:
+		return http.StatusInternalServerError
+	}
+}
+
+func (code RPCErrorCode) Error() string {
+	return code.String()
+}
diff --git a/rpc/lib/types/types.go b/rpc/lib/types/types.go
index 42a7a219..8ff3e6c6 100644
--- a/rpc/lib/types/types.go
+++ b/rpc/lib/types/types.go
@@ -71,9 +71,9 @@ func ArrayToRequest(id string, method string, params []interface{}) (RPCRequest,
 // RESPONSE
 
 type RPCError struct {
-	Code    int    `json:"code"`
-	Message string `json:"message"`
-	Data    string `json:"data,omitempty"`
+	Code    RPCErrorCode `json:"code"`
+	Message string       `json:"message"`
+	Data    string       `json:"data,omitempty"`
 }
 
 func (err RPCError) Error() string {
@@ -84,6 +84,13 @@ func (err RPCError) Error() string {
 	return fmt.Sprintf(baseFormat, err.Code, err.Message)
 }
 
+func (err *RPCError) HTTPStatusCode() int {
+	if err == nil {
+		return 200
+	}
+	return err.Code.HTTPStatusCode()
+}
+
 type RPCResponse struct {
 	JSONRPC string          `json:"jsonrpc"`
 	ID      string          `json:"id"`
@@ -106,11 +113,11 @@ func NewRPCSuccessResponse(id string, res interface{}) RPCResponse {
 	return RPCResponse{JSONRPC: "2.0", ID: id, Result: rawMsg}
 }
 
-func NewRPCErrorResponse(id string, code int, msg string, data string) RPCResponse {
+func NewRPCErrorResponse(id string, code RPCErrorCode, data string) RPCResponse {
 	return RPCResponse{
 		JSONRPC: "2.0",
 		ID:      id,
-		Error:   &RPCError{Code: code, Message: msg, Data: data},
+		Error:   &RPCError{Code: code, Message: code.String(), Data: data},
 	}
 }
 
@@ -122,27 +129,27 @@ func (resp RPCResponse) String() string {
 }
 
 func RPCParseError(id string, err error) RPCResponse {
-	return NewRPCErrorResponse(id, -32700, "Parse error. Invalid JSON", err.Error())
+	return NewRPCErrorResponse(id, RPCErrorCodeParseError, err.Error())
 }
 
 func RPCInvalidRequestError(id string, err error) RPCResponse {
-	return NewRPCErrorResponse(id, -32600, "Invalid Request", err.Error())
+	return NewRPCErrorResponse(id, RPCErrorCodeInvalidRequest, err.Error())
 }
 
 func RPCMethodNotFoundError(id string) RPCResponse {
-	return NewRPCErrorResponse(id, -32601, "Method not found", "")
+	return NewRPCErrorResponse(id, RPCErrorCodeMethodNotFound, "")
 }
 
 func RPCInvalidParamsError(id string, err error) RPCResponse {
-	return NewRPCErrorResponse(id, -32602, "Invalid params", err.Error())
+	return NewRPCErrorResponse(id, RPCErrorCodeInvalidParams, err.Error())
 }
 
 func RPCInternalError(id string, err error) RPCResponse {
-	return NewRPCErrorResponse(id, -32603, "Internal error", err.Error())
+	return NewRPCErrorResponse(id, RPCErrorCodeInternalError, err.Error())
 }
 
 func RPCServerError(id string, err error) RPCResponse {
-	return NewRPCErrorResponse(id, -32000, "Server error", err.Error())
+	return NewRPCErrorResponse(id, RPCErrorCodeServerError, err.Error())
 }
 
 //----------------------------------------
diff --git a/rpc/lib/types/types_test.go b/rpc/lib/types/types_test.go
index 1227bbd1..dd0e0316 100644
--- a/rpc/lib/types/types_test.go
+++ b/rpc/lib/types/types_test.go
@@ -23,12 +23,12 @@ func TestResponses(t *testing.T) {
 
 	d := RPCParseError("1", errors.New("Hello world"))
 	e, _ := json.Marshal(d)
-	f := `{"jsonrpc":"2.0","id":"1","error":{"code":-32700,"message":"Parse error. Invalid JSON","data":"Hello world"}}`
+	f := `{"jsonrpc":"2.0","id":"1","error":{"code":-32700,"message":"Parse Error","data":"Hello world"}}`
 	assert.Equal(string(f), string(e))
 
 	g := RPCMethodNotFoundError("2")
 	h, _ := json.Marshal(g)
-	i := `{"jsonrpc":"2.0","id":"2","error":{"code":-32601,"message":"Method not found"}}`
+	i := `{"jsonrpc":"2.0","id":"2","error":{"code":-32601,"message":"Method Not Found"}}`
 	assert.Equal(string(h), string(i))
 }
 
diff --git a/rpc/result.go b/rpc/result.go
index 3159690e..44a61c1d 100644
--- a/rpc/result.go
+++ b/rpc/result.go
@@ -18,6 +18,8 @@ import (
 	"encoding/json"
 	"fmt"
 
+	"time"
+
 	acm "github.com/hyperledger/burrow/account"
 	"github.com/hyperledger/burrow/binary"
 	"github.com/hyperledger/burrow/crypto"
@@ -120,6 +122,12 @@ type ResultStatus struct {
 	NodeVersion       string
 }
 
+type ResultLastBlockInfo struct {
+	LastBlockHeight uint64
+	LastBlockTime   time.Time
+	LastBlockHash   binary.HexBytes
+}
+
 type ResultChainId struct {
 	ChainName   string
 	ChainId     string
diff --git a/rpc/result_test.go b/rpc/result_test.go
index 62ea5b51..ade8a615 100644
--- a/rpc/result_test.go
+++ b/rpc/result_test.go
@@ -18,7 +18,12 @@ import (
 	"encoding/json"
 	"testing"
 
+	"time"
+
+	"fmt"
+
 	acm "github.com/hyperledger/burrow/account"
+	"github.com/hyperledger/burrow/binary"
 	"github.com/hyperledger/burrow/crypto"
 	"github.com/hyperledger/burrow/execution"
 	"github.com/hyperledger/burrow/txs"
@@ -166,3 +171,15 @@ func TestResultDumpConsensusState(t *testing.T) {
 	require.NoError(t, err)
 	assert.Equal(t, string(bs), string(bsOut))
 }
+
+func TestResultLastBlockInfo(t *testing.T) {
+	res := &ResultLastBlockInfo{
+		LastBlockTime:   time.Now(),
+		LastBlockHash:   binary.HexBytes{3, 4, 5, 6},
+		LastBlockHeight: 2343,
+	}
+	bs, err := json.Marshal(res)
+	require.NoError(t, err)
+	fmt.Println(string(bs))
+
+}
diff --git a/rpc/service.go b/rpc/service.go
index 81afee60..0863a65a 100644
--- a/rpc/service.go
+++ b/rpc/service.go
@@ -18,6 +18,10 @@ import (
 	"context"
 	"fmt"
 
+	"time"
+
+	"encoding/json"
+
 	acm "github.com/hyperledger/burrow/account"
 	"github.com/hyperledger/burrow/account/state"
 	"github.com/hyperledger/burrow/binary"
@@ -418,3 +422,33 @@ func (s *Service) GeneratePrivateAccount() (*ResultGeneratePrivateAccount, error
 		PrivateAccount: acm.AsConcretePrivateAccount(privateAccount),
 	}, nil
 }
+
+func (s *Service) LastBlockInfo(blockWithin string) (*ResultLastBlockInfo, error) {
+	res := &ResultLastBlockInfo{
+		LastBlockHeight: s.blockchain.LastBlockHeight(),
+		LastBlockHash:   s.blockchain.LastBlockHash(),
+		LastBlockTime:   s.blockchain.LastBlockTime(),
+	}
+	if blockWithin == "" {
+		return res, nil
+	}
+	duration, err := time.ParseDuration(blockWithin)
+	if err != nil {
+		return nil, fmt.Errorf("could not parse blockWithin duration to determine whether to throw error: %v", err)
+	}
+	// Take neg abs in case caller is counting backwards (not we add later)
+	if duration > 0 {
+		duration = -duration
+	}
+	blockTimeThreshold := time.Now().Add(duration)
+	if res.LastBlockTime.After(blockTimeThreshold) {
+		// We've created blocks recently enough
+		return res, nil
+	}
+	resJSON, err := json.Marshal(res)
+	if err != nil {
+		resJSON = []byte("<error: could not marshal last block info>")
+	}
+	return nil, fmt.Errorf("no block committed within the last %s (cutoff: %s), last block info: %s",
+		blockWithin, blockTimeThreshold.Format(time.RFC3339), string(resJSON))
+}
diff --git a/rpc/tm/methods.go b/rpc/tm/methods.go
index 6e892df8..017cf29c 100644
--- a/rpc/tm/methods.go
+++ b/rpc/tm/methods.go
@@ -55,6 +55,9 @@ const (
 	// Private keys and signing
 	GeneratePrivateAccount = "unsafe/gen_priv_account"
 	SignTx                 = "unsafe/sign_tx"
+
+	// Health check
+	LastBlockInfo = "last_block_info"
 )
 
 const SubscriptionTimeout = 5 * time.Second
@@ -175,6 +178,7 @@ func GetRoutes(service *rpc.Service, logger *logging.Logger) map[string]*server.
 
 		// Private account
 		GeneratePrivateAccount: server.NewRPCFunc(service.GeneratePrivateAccount, ""),
+		LastBlockInfo:          server.NewRPCFunc(service.LastBlockInfo, "block_within"),
 	}
 }
 
-- 
GitLab