Repository: nspcc-dev/neo-go
Pull request #3939 (2025-06-18 11:35:53)

rpcclient: add missing GetBlockHeaderByIndex[Verbose] APIs
Requested by @carpawell, will be useful for NeoFS node.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/nspcc-dev/neo-go/pull/3939?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 81.87%. Comparing base [(`05163de`)](https://app.codecov.io/gh/nspcc-dev/neo-go/commit/05163ded7df3600bd2d8ec13e6065e54f57bf21e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev) to head [(`acfeefb`)](https://app.codecov.io/gh/nspcc-dev/neo-go/commit/acfeefb54621300f3e623650d51cc870abb14daa?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #3939      +/-   ##
==========================================
+ Coverage   81.78%   81.87%   +0.09%     
==========================================
  Files         345      345              
  Lines       48921    48929       +8     
==========================================
+ Hits        40010    40062      +52     
+ Misses       7242     7199      -43     
+ Partials     1669     1668       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/nspcc-dev/neo-go/pull/3939?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/ROADMAP.md b/ROADMAP.md
index 49fb19dfe..22fae5017 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -7,12 +7,12 @@ functionality.
 ## Versions 0.7X.Y (as needed)
 * Neo 2.0 support (bug fixes, minor functionality additions)
 
-## Version 0.109.0 (~April 2025)
+## Version 0.111.0 (~Aug 2025)
  * protocol updates
  * bug fixes
  * NeoFS-based synchronization
 
-## Version 1.0 (2024, TBD)
+## Version 1.0 (2025, TBD)
  * stable version
 
 # Deprecated functionality
@@ -24,3 +24,11 @@ APIs/commands/configurations will be removed and here is a list of scheduled
 breaking changes. Consider changing your code/scripts/configurations if you're
 using anything mentioned here.
 
+## GetBlockHeader and GetBlockHeaderVerbose methods of RPCClient
+
+GetBlockHeader and GetBlockHeaderVerbose were replaced by GetBlockHeaderByHash
+and GetBlockHeaderByHashVerbose methods respectively to follow RPCClient
+naming convention. No functional changes implied.
+
+Removal of GetBlockHeader and GetBlockHeaderVerbose methods is scheduled for
+0.112.0 release.
\ No newline at end of file
diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go
index 04538b1af..71134acc5 100644
--- a/pkg/rpcclient/rpc.go
+++ b/pkg/rpcclient/rpc.go
@@ -151,12 +151,31 @@ func (c *Client) GetBlockHash(index uint32) (util.Uint256, error) {
 	return resp, nil
 }
 
-// GetBlockHeader returns the corresponding block header information from a serialized hex string
-// according to the specified script hash. In-header stateroot option must be
-// initialized with Init before calling this method.
+// GetBlockHeader returns the corresponding block header information from
+// a serialized base64 string according to the specified script hash. In-header
+// stateroot option must be initialized with Init before calling this method.
+// Deprecated: Use GetBlockHeaderByHash instead.
 func (c *Client) GetBlockHeader(hash util.Uint256) (*block.Header, error) {
+	return c.GetBlockHeaderByHash(hash)
+}
+
+// GetBlockHeaderByHash returns the corresponding block header information from
+// a serialized base64 string according to the specified script hash. In-header
+// stateroot option must be initialized with Init before calling this method.
+func (c *Client) GetBlockHeaderByHash(hash util.Uint256) (*block.Header, error) {
+	return c.getHeader(hash.StringLE())
+}
+
+// GetBlockHeaderByIndex returns the corresponding block header information from
+// a serialized base64 string according to the specified index. In-header
+// stateroot option must be initialized with Init before calling this method.
+func (c *Client) GetBlockHeaderByIndex(index uint32) (*block.Header, error) {
+	return c.getHeader(index)
+}
+
+func (c *Client) getHeader(param any) (*block.Header, error) {
 	var (
-		params = []any{hash.StringLE()}
+		params = []any{param}
 		resp   []byte
 		h      *block.Header
 	)
@@ -186,12 +205,32 @@ func (c *Client) GetBlockHeaderCount() (uint32, error) {
 	return resp, nil
 }
 
-// GetBlockHeaderVerbose returns the corresponding block header information from a Json format string
-// according to the specified script hash. In-header stateroot option must be
-// initialized with Init before calling this method.
+// GetBlockHeaderVerbose returns the corresponding block header information from
+// a JSON format string according to the specified script hash. In-header
+// stateroot option must be initialized with Init before calling this method.
+// Deprecated: Use GetBlockHeaderByHashVerbose instead.
 func (c *Client) GetBlockHeaderVerbose(hash util.Uint256) (*result.Header, error) {
+	return c.GetBlockHeaderByHashVerbose(hash)
+}
+
+// GetBlockHeaderByHashVerbose returns the corresponding block header information from
+// a JSON format string according to the specified script hash. In-header
+// stateroot option must be initialized with Init before calling this method.
+func (c *Client) GetBlockHeaderByHashVerbose(hash util.Uint256) (*result.Header, error) {
+	return c.getHeaderVerbose(hash.StringLE())
+}
+
+// GetBlockHeaderByIndexVerbose returns the corresponding block header
+// information from a JSON format string according to the specified index.
+// In-header stateroot option must be initialized with Init before calling this
+// method.
+func (c *Client) GetBlockHeaderByIndexVerbose(index uint32) (*result.Header, error) {
+	return c.getHeaderVerbose(index)
+}
+
+func (c *Client) getHeaderVerbose(param any) (*result.Header, error) {
 	var (
-		params = []any{hash.StringLE(), 1}
+		params = []any{param, 1}
 		resp   = &result.Header{}
 	)
 	if err := c.performRequest("getblockheader", params, resp); err != nil {


Test patch:
diff --git a/pkg/rpcclient/rpc_test.go b/pkg/rpcclient/rpc_test.go
index fc8a31525..c579bef5a 100644
--- a/pkg/rpcclient/rpc_test.go
+++ b/pkg/rpcclient/rpc_test.go
@@ -238,7 +238,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
 	},
 	"getblockheader": {
 		{
-			name: "positive",
+			name: "byHash_positive",
 			invoke: func(c *Client) (any, error) {
 				hash, err := util.Uint256DecodeStringLE("68e4bd688b852e807eef13a0ff7da7b02223e359a35153667e88f9cb4a3b0801")
 				if err != nil {
@@ -253,13 +253,42 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
 			},
 		},
 		{
-			name: "verbose_positive",
+			name: "byHash_verbose_positive",
 			invoke: func(c *Client) (i any, err error) {
 				hash, err := util.Uint256DecodeStringLE("cbb73ed9e31dc41a8a222749de475e6ebc2a73b99f73b091a72e0b146110fe86")
 				if err != nil {
 					panic(err)
 				}
-				return c.GetBlockHeaderVerbose(hash)
+				return c.GetBlockHeaderByHashVerbose(hash)
+			},
+			serverResponse: `{"id":1,"jsonrpc":"2.0","result":` + header1Verbose + `}`,
+			result: func(c *Client) any {
+				b := getResultBlock1()
+				return &result.Header{
+					Header: b.Header,
+					BlockMetadata: result.BlockMetadata{
+						Size:          457,
+						NextBlockHash: b.NextBlockHash,
+						Confirmations: b.Confirmations,
+					},
+				}
+			},
+		},
+		{
+			name: "byIndex_positive",
+			invoke: func(c *Client) (any, error) {
+				return c.GetBlockHeaderByIndex(1)
+			},
+			serverResponse: `{"id":1,"jsonrpc":"2.0","result":"` + base64Header1 + `"}`,
+			result: func(c *Client) any {
+				b := getResultBlock1()
+				return &b.Header
+			},
+		},
+		{
+			name: "byIndex_verbose_positive",
+			invoke: func(c *Client) (i any, err error) {
+				return c.GetBlockHeaderByIndexVerbose(1)
 			},
 			serverResponse: `{"id":1,"jsonrpc":"2.0","result":` + header1Verbose + `}`,
 			result: func(c *Client) any {
@@ -1545,7 +1574,7 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{
 				if err != nil {
 					panic(err)
 				}
-				return c.GetBlockHeader(hash)
+				return c.GetBlockHeaderByHash(hash)
 			},
 		},
 		{
@@ -1597,7 +1626,13 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{
 				if err != nil {
 					panic(err)
 				}
-				return c.GetBlockHeader(hash)
+				return c.GetBlockHeaderByHash(hash)
+			},
+		},
+		{
+			name: "getheaderbyindex_decodebin_err",
+			invoke: func(c *Client) (any, error) {
+				return c.GetBlockHeaderByIndex(1)
 			},
 		},
 		{
@@ -1681,13 +1716,25 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{
 		{
 			name: "getblockheader_invalid_params_error",
 			invoke: func(c *Client) (any, error) {
-				return c.GetBlockHeader(util.Uint256{})
+				return c.GetBlockHeaderByHash(util.Uint256{})
+			},
+		},
+		{
+			name: "getblockheaderbyindex_invalid_params_error",
+			invoke: func(c *Client) (any, error) {
+				return c.GetBlockHeaderByIndex(1)
 			},
 		},
 		{
 			name: "getblockheader_verbose_invalid_params_error",
 			invoke: func(c *Client) (any, error) {
-				return c.GetBlockHeaderVerbose(util.Uint256{})
+				return c.GetBlockHeaderByHashVerbose(util.Uint256{})
+			},
+		},
+		{
+			name: "getblockheaderbyindex_verbose_invalid_params_error",
+			invoke: func(c *Client) (any, error) {
+				return c.GetBlockHeaderByIndexVerbose(1)
 			},
 		},
 		{
@@ -1880,13 +1927,25 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{
 		{
 			name: "getblockheader_unmarshalling_error",
 			invoke: func(c *Client) (any, error) {
-				return c.GetBlockHeader(util.Uint256{})
+				return c.GetBlockHeaderByHash(util.Uint256{})
+			},
+		},
+		{
+			name: "getblockheaderbyindex_unmarshalling_error",
+			invoke: func(c *Client) (any, error) {
+				return c.GetBlockHeaderByIndex(1)
 			},
 		},
 		{
 			name: "getblockheader_verbose_unmarshalling_error",
 			invoke: func(c *Client) (any, error) {
-				return c.GetBlockHeaderVerbose(util.Uint256{})
+				return c.GetBlockHeaderByHashVerbose(util.Uint256{})
+			},
+		},
+		{
+			name: "getblockheaderbyindex_verbose_unmarshalling_error",
+			invoke: func(c *Client) (any, error) {
+				return c.GetBlockHeaderByIndexVerbose(1)
 			},
 		},
 		{
@@ -2213,7 +2272,13 @@ func TestUninitedClient(t *testing.T) {
 	require.Error(t, err)
 	_, err = c.GetBlockByIndexVerbose(0)
 	require.Error(t, err)
-	_, err = c.GetBlockHeader(util.Uint256{})
+	_, err = c.GetBlockHeaderByHash(util.Uint256{})
+	require.Error(t, err)
+	_, err = c.GetBlockHeaderByHashVerbose(util.Uint256{})
+	require.Error(t, err)
+	_, err = c.GetBlockHeaderByIndex(1)
+	require.Error(t, err)
+	_, err = c.GetBlockHeaderByIndexVerbose(1)
 	require.Error(t, err)
 	_, err = c.GetRawTransaction(util.Uint256{})
 	require.Error(t, err)
diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go
index 20419b192..1d7da1dab 100644
--- a/pkg/services/rpcsrv/client_test.go
+++ b/pkg/services/rpcsrv/client_test.go
@@ -2808,3 +2808,47 @@ func TestClient_InvokeContainedScript(t *testing.T) {
 		})
 	}
 }
+
+func TestClient_GetBlockHeader(t *testing.T) {
+	chain, _, httpSrv := initClearServerWithInMemoryChain(t)
+	c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{})
+	require.NoError(t, err)
+	t.Cleanup(c.Close)
+	require.NoError(t, c.Init())
+
+	expected, err := chain.GetHeader(chain.GetHeaderHash(0))
+	require.NoError(t, err)
+	w := io.NewBufBinWriter()
+	expected.EncodeBinary(w.BinWriter)
+	require.NoError(t, w.Err)
+	size := len(w.Bytes())
+	expectedRes := &result.Header{
+		Header: *expected,
+		BlockMetadata: result.BlockMetadata{
+			Size:          size,
+			NextBlockHash: nil,
+			Confirmations: 1,
+		},
+	}
+
+	t.Run("by hash", func(t *testing.T) {
+		h, err := c.GetBlockHeaderByHash(expected.Hash())
+		require.NoError(t, err)
+		require.Equal(t, expected, h)
+	})
+	t.Run("by hash verbose", func(t *testing.T) {
+		h, err := c.GetBlockHeaderByHashVerbose(expected.Hash())
+		require.NoError(t, err)
+		require.Equal(t, expectedRes, h)
+	})
+	t.Run("by index", func(t *testing.T) {
+		h, err := c.GetBlockHeaderByIndex(0)
+		require.NoError(t, err)
+		require.Equal(t, expected, h)
+	})
+	t.Run("by index verbose", func(t *testing.T) {
+		h, err := c.GetBlockHeaderByIndexVerbose(0)
+		require.NoError(t, err)
+		require.Equal(t, expectedRes, h)
+	})
+}