From 42e2aff3815aa04c61d688c80e601295bb1b36bf Mon Sep 17 00:00:00 2001
From: Anna Shaleva <shaleva.ann@nspcc.ru>
Date: Wed, 4 Mar 2020 20:35:37 +0300
Subject: [PATCH] rpc: implement getblockheader RPC

closes #711
---
 docs/rpc.md                             |  2 +-
 pkg/rpc/response/result/block_header.go | 53 +++++++++++++++++++
 pkg/rpc/server/prometheus.go            |  9 ++++
 pkg/rpc/server/server.go                | 43 ++++++++++++++++
 pkg/rpc/server/server_test.go           | 68 +++++++++++++++++++++++++
 5 files changed, 174 insertions(+), 1 deletion(-)
 create mode 100644 pkg/rpc/response/result/block_header.go

diff --git a/docs/rpc.md b/docs/rpc.md
index 6781a18f1..81bb75073 100644
--- a/docs/rpc.md
+++ b/docs/rpc.md
@@ -42,7 +42,7 @@ which would yield the response:
 | `getblock` | Yes |
 | `getblockcount` | Yes |
 | `getblockhash` | Yes |
-| `getblockheader` | No (#711) |
+| `getblockheader` | Yes |
 | `getblocksysfee` | Yes |
 | `getclaimable` | Yes |
 | `getconnectioncount` | Yes |
diff --git a/pkg/rpc/response/result/block_header.go b/pkg/rpc/response/result/block_header.go
new file mode 100644
index 000000000..331600d3f
--- /dev/null
+++ b/pkg/rpc/response/result/block_header.go
@@ -0,0 +1,53 @@
+package result
+
+import (
+	"strconv"
+
+	"github.com/nspcc-dev/neo-go/pkg/core"
+	"github.com/nspcc-dev/neo-go/pkg/core/block"
+	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
+	"github.com/nspcc-dev/neo-go/pkg/io"
+	"github.com/nspcc-dev/neo-go/pkg/util"
+)
+
+type (
+	// Header wrapper used for the representation of
+	// block header on the RPC Server.
+	Header struct {
+		Hash          util.Uint256        `json:"hash"`
+		Size          int                 `json:"size"`
+		Version       uint32              `json:"version"`
+		PrevBlockHash util.Uint256        `json:"previousblockhash"`
+		MerkleRoot    util.Uint256        `json:"merkleroot"`
+		Timestamp     uint32              `json:"time"`
+		Index         uint32              `json:"index"`
+		Nonce         string              `json:"nonce"`
+		NextConsensus util.Uint160        `json:"nextconsensus"`
+		Script        transaction.Witness `json:"script"`
+		Confirmations uint32              `json:"confirmations"`
+		NextBlockHash *util.Uint256       `json:"nextblockhash,omitempty"`
+	}
+)
+
+// NewHeader creates a new Header wrapper.
+func NewHeader(h *block.Header, chain core.Blockchainer) Header {
+	res := Header{
+		Hash:          h.Hash(),
+		Size:          io.GetVarSize(h),
+		Version:       h.Version,
+		PrevBlockHash: h.PrevHash,
+		MerkleRoot:    h.MerkleRoot,
+		Timestamp:     h.Timestamp,
+		Index:         h.Index,
+		Nonce:         strconv.FormatUint(h.ConsensusData, 16),
+		NextConsensus: h.NextConsensus,
+		Script:        h.Script,
+		Confirmations: chain.BlockHeight() - h.Index + 1,
+	}
+
+	hash := chain.GetHeaderHash(int(h.Index) + 1)
+	if !hash.Equals(util.Uint256{}) {
+		res.NextBlockHash = &hash
+	}
+	return res
+}
diff --git a/pkg/rpc/server/prometheus.go b/pkg/rpc/server/prometheus.go
index b406761ba..8e1b8c950 100644
--- a/pkg/rpc/server/prometheus.go
+++ b/pkg/rpc/server/prometheus.go
@@ -43,6 +43,14 @@ var (
 		},
 	)
 
+	getblockheaderCalled = prometheus.NewCounter(
+		prometheus.CounterOpts{
+			Help:      "Number of calls to getblockheader rpc endpoint",
+			Name:      "getblockheader_called",
+			Namespace: "neogo",
+		},
+	)
+
 	getblocksysfeeCalled = prometheus.NewCounter(
 		prometheus.CounterOpts{
 			Help:      "Number of calls to getblocksysfee rpc endpoint",
@@ -171,6 +179,7 @@ func init() {
 		getbestblockCalled,
 		getblockcountCalled,
 		getblockHashCalled,
+		getblockheaderCalled,
 		getblocksysfeeCalled,
 		getconnectioncountCalled,
 		getcontractstateCalled,
diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go
index 5a47cd7c6..7e02f0d7d 100644
--- a/pkg/rpc/server/server.go
+++ b/pkg/rpc/server/server.go
@@ -186,6 +186,10 @@ Methods:
 
 		results = s.chain.GetHeaderHash(num)
 
+	case "getblockheader":
+		getblockheaderCalled.Inc()
+		results, resultsErr = s.getBlockHeader(reqParams)
+
 	case "getblocksysfee":
 		getblocksysfeeCalled.Inc()
 		results, resultsErr = s.getBlockSysFee(reqParams)
@@ -557,6 +561,45 @@ func (s *Server) getBlockSysFee(reqParams request.Params) (util.Fixed8, error) {
 	return blockSysFee, nil
 }
 
+// getBlockHeader returns the corresponding block header information according to the specified script hash.
+func (s *Server) getBlockHeader(reqParams request.Params) (interface{}, error) {
+	var verbose bool
+
+	param, ok := reqParams.ValueWithType(0, request.StringT)
+	if !ok {
+		return nil, response.ErrInvalidParams
+	}
+	hash, err := param.GetUint256()
+	if err != nil {
+		return nil, response.ErrInvalidParams
+	}
+
+	param, ok = reqParams.ValueWithType(1, request.NumberT)
+	if ok {
+		v, err := param.GetInt()
+		if err != nil {
+			return nil, response.ErrInvalidParams
+		}
+		verbose = v != 0
+	}
+
+	h, err := s.chain.GetHeader(hash)
+	if err != nil {
+		return nil, response.NewRPCError("unknown block", "", nil)
+	}
+
+	if verbose {
+		return result.NewHeader(h, s.chain), nil
+	}
+
+	buf := io.NewBufBinWriter()
+	h.EncodeBinary(buf.BinWriter)
+	if buf.Err != nil {
+		return nil, err
+	}
+	return hex.EncodeToString(buf.Bytes()), nil
+}
+
 // invoke implements the `invoke` RPC call.
 func (s *Server) invoke(reqParams request.Params) (interface{}, error) {
 	scriptHashHex, ok := reqParams.ValueWithType(0, request.StringT)
diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go
index 05a93da14..2e0bd43ad 100644
--- a/pkg/rpc/server/server_test.go
+++ b/pkg/rpc/server/server_test.go
@@ -9,12 +9,14 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"reflect"
+	"strconv"
 	"strings"
 	"testing"
 
 	"github.com/nspcc-dev/neo-go/pkg/core"
 	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
 	"github.com/nspcc-dev/neo-go/pkg/internal/random"
+	"github.com/nspcc-dev/neo-go/pkg/io"
 	"github.com/nspcc-dev/neo-go/pkg/rpc/response"
 	"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
 	"github.com/nspcc-dev/neo-go/pkg/util"
@@ -334,6 +336,72 @@ var rpcTestCases = map[string][]rpcTestCase{
 			fail:   true,
 		},
 	},
+	"getblockheader": {
+		{
+			name:   "positive, no verbose",
+			params: `["a6e526375a780335112299f2262501e5e9574c3ba61b16bbc1e282b344f6c14d"]`,
+			result: func(e *executor) interface{} {
+				expected := "00000000b32b4a122730deb7f862bdc99b45cc8ef12ae55a8096a344429968702091bf3a8e9f458e580420a99a67f0d4137266f76523ea618d1db7ec314b106eb6e67c1721bd575e340000005704000000000000be48d3a3f5d10013ab9ffee489706078714f1ea201fd04014092cb08b7fbef9b8fe47e7d7ec0557e32aeb2e61bdf5c1c6cac203ed12ad32d50629af1783436e585acc8581f46b2b29247f04102d66e9e7e112ae5444c46487340896a200e806f4597df05a12f91f0fbc5c256522687547e7e057b88ec14082213ce98a88f6fd312d3e3fad4b77db1fcc95af69282d887d56f461280df557e4820402903ff86e02559a58376da45d27eb24e5362f6fd922b79a55c9927e33a81265b5ccfef9db83a48b3597e9b576999fc5c0f02df982dbf871a1ef60b089ffb4dae40405d746c546d0ac3d7e36f61d71996f104884db93cca7499f687eb8b8e444327f97bfb05e49ee388d36e0dc73132a3a9ec24a6d8c8b27ae92223dbb7b06af1ec8b532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae00"
+				return &expected
+			},
+		},
+		{
+			name:   "positive, verbose 0",
+			params: `["a6e526375a780335112299f2262501e5e9574c3ba61b16bbc1e282b344f6c14d", 0]`,
+			result: func(e *executor) interface{} {
+				expected := "00000000b32b4a122730deb7f862bdc99b45cc8ef12ae55a8096a344429968702091bf3a8e9f458e580420a99a67f0d4137266f76523ea618d1db7ec314b106eb6e67c1721bd575e340000005704000000000000be48d3a3f5d10013ab9ffee489706078714f1ea201fd04014092cb08b7fbef9b8fe47e7d7ec0557e32aeb2e61bdf5c1c6cac203ed12ad32d50629af1783436e585acc8581f46b2b29247f04102d66e9e7e112ae5444c46487340896a200e806f4597df05a12f91f0fbc5c256522687547e7e057b88ec14082213ce98a88f6fd312d3e3fad4b77db1fcc95af69282d887d56f461280df557e4820402903ff86e02559a58376da45d27eb24e5362f6fd922b79a55c9927e33a81265b5ccfef9db83a48b3597e9b576999fc5c0f02df982dbf871a1ef60b089ffb4dae40405d746c546d0ac3d7e36f61d71996f104884db93cca7499f687eb8b8e444327f97bfb05e49ee388d36e0dc73132a3a9ec24a6d8c8b27ae92223dbb7b06af1ec8b532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae00"
+				return &expected
+			},
+		},
+		{
+			name:   "positive, verbose !=0",
+			params: `["a6e526375a780335112299f2262501e5e9574c3ba61b16bbc1e282b344f6c14d", 2]`,
+			result: func(e *executor) interface{} {
+				hash, _ := util.Uint256DecodeStringLE("a6e526375a780335112299f2262501e5e9574c3ba61b16bbc1e282b344f6c14d")
+				block, _ := e.chain.GetBlock(hash)
+				header := block.Header()
+				expected := result.Header{
+					Hash:          header.Hash(),
+					Size:          io.GetVarSize(header),
+					Version:       header.Version,
+					PrevBlockHash: header.PrevHash,
+					MerkleRoot:    header.MerkleRoot,
+					Timestamp:     header.Timestamp,
+					Index:         header.Index,
+					Nonce:         strconv.FormatUint(header.ConsensusData, 16),
+					NextConsensus: header.NextConsensus,
+					Script:        header.Script,
+					Confirmations: e.chain.BlockHeight() - header.Index + 1,
+				}
+
+				nextHash := e.chain.GetHeaderHash(int(header.Index) + 1)
+				if !hash.Equals(util.Uint256{}) {
+					expected.NextBlockHash = &nextHash
+				}
+				return &expected
+			},
+		},
+		{
+			name:   "invalid verbose type",
+			params: `["a6e526375a780335112299f2262501e5e9574c3ba61b16bbc1e282b344f6c14d", true]`,
+			fail:   true,
+		},
+		{
+			name:   "invalid block hash",
+			params: `["notahash"]`,
+			fail:   true,
+		},
+		{
+			name:   "unknown block",
+			params: `["a6e526375a780335112299f2262501e5e9574c3ba61b16bbc1e282b344f6c141"]`,
+			fail:   true,
+		},
+		{
+			name:   "no params",
+			params: `[]`,
+			fail:   true,
+		},
+	},
 	"getblocksysfee": {
 		{
 			name:   "positive",