From b4396d79f203b1a801a41c6b04a95b2b4d59aad4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 2 Dec 2021 14:13:48 +0100 Subject: [PATCH] rpc: implement header and header_by_hash queries (backport #7270) (#7367) --- CHANGELOG_PENDING.md | 2 + internal/consensus/replay_test.go | 1 + internal/rpc/core/blocks.go | 35 +- internal/rpc/core/blocks_test.go | 25 +- internal/rpc/core/routes.go | 2 + internal/state/mocks/block_store.go | 16 + internal/state/services.go | 1 + internal/store/store.go | 20 + light/proxy/routes.go | 18 + light/rpc/client.go | 37 +- rpc/client/http/http.go | 33 +- rpc/client/interface.go | 2 + rpc/client/local/local.go | 8 + rpc/client/mocks/client.go | 802 ++++++++++++++++++++++++++++ rpc/client/rpc_test.go | 9 + rpc/coretypes/responses.go | 5 + rpc/openapi/openapi.yaml | 80 ++- 17 files changed, 1064 insertions(+), 32 deletions(-) create mode 100644 rpc/client/mocks/client.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 29d5a832f..8ab7fd7e1 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -26,6 +26,8 @@ Special thanks to external contributors on this release: ### FEATURES +- [rpc] [\#7270](https://github.com/tendermint/tendermint/pull/7270) Add `header` and `header_by_hash` RPC Client queries. (@fedekunze) (@cmwaters) + ### IMPROVEMENTS - [\#7338](https://github.com/tendermint/tendermint/pull/7338) pubsub: Performance improvements for the event query API (backport of #7319) (@creachadair) diff --git a/internal/consensus/replay_test.go b/internal/consensus/replay_test.go index f72da903b..42c449a05 100644 --- a/internal/consensus/replay_test.go +++ b/internal/consensus/replay_test.go @@ -1195,6 +1195,7 @@ func (bs *mockBlockStore) LoadBlock(height int64) *types.Block { return bs.chain func (bs *mockBlockStore) LoadBlockByHash(hash []byte) *types.Block { return bs.chain[int64(len(bs.chain))-1] } +func (bs *mockBlockStore) LoadBlockMetaByHash(hash []byte) *types.BlockMeta { return nil } func (bs *mockBlockStore) LoadBlockMeta(height int64) *types.BlockMeta { block := bs.chain[height-1] return &types.BlockMeta{ diff --git a/internal/rpc/core/blocks.go b/internal/rpc/core/blocks.go index 26472fab4..2e7f24726 100644 --- a/internal/rpc/core/blocks.go +++ b/internal/rpc/core/blocks.go @@ -51,7 +51,8 @@ func (env *Environment) BlockchainInfo( return &coretypes.ResultBlockchainInfo{ LastHeight: env.BlockStore.Height(), - BlockMetas: blockMetas}, nil + BlockMetas: blockMetas, + }, nil } // error if either min or max are negative or min > max @@ -122,6 +123,38 @@ func (env *Environment) BlockByHash(ctx *rpctypes.Context, hash bytes.HexBytes) return &coretypes.ResultBlock{BlockID: blockMeta.BlockID, Block: block}, nil } +// Header gets block header at a given height. +// If no height is provided, it will fetch the latest header. +// More: https://docs.tendermint.com/master/rpc/#/Info/header +func (env *Environment) Header(ctx *rpctypes.Context, heightPtr *int64) (*coretypes.ResultHeader, error) { + height, err := env.getHeight(env.BlockStore.Height(), heightPtr) + if err != nil { + return nil, err + } + + blockMeta := env.BlockStore.LoadBlockMeta(height) + if blockMeta == nil { + return &coretypes.ResultHeader{}, nil + } + + return &coretypes.ResultHeader{Header: &blockMeta.Header}, nil +} + +// HeaderByHash gets header by hash. +// More: https://docs.tendermint.com/master/rpc/#/Info/header_by_hash +func (env *Environment) HeaderByHash(ctx *rpctypes.Context, hash bytes.HexBytes) (*coretypes.ResultHeader, error) { + // N.B. The hash parameter is HexBytes so that the reflective parameter + // decoding logic in the HTTP service will correctly translate from JSON. + // See https://github.com/tendermint/tendermint/issues/6802 for context. + + blockMeta := env.BlockStore.LoadBlockMetaByHash(hash) + if blockMeta == nil { + return &coretypes.ResultHeader{}, nil + } + + return &coretypes.ResultHeader{Header: &blockMeta.Header}, nil +} + // Commit gets block commit at a given height. // If no height is provided, it will fetch the commit for the latest block. // More: https://docs.tendermint.com/master/rpc/#/Info/commit diff --git a/internal/rpc/core/blocks_test.go b/internal/rpc/core/blocks_test.go index 68237bc0b..213845bf4 100644 --- a/internal/rpc/core/blocks_test.go +++ b/internal/rpc/core/blocks_test.go @@ -11,10 +11,10 @@ import ( abci "github.com/tendermint/tendermint/abci/types" sm "github.com/tendermint/tendermint/internal/state" + "github.com/tendermint/tendermint/internal/state/mocks" tmstate "github.com/tendermint/tendermint/proto/tendermint/state" "github.com/tendermint/tendermint/rpc/coretypes" rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types" - "github.com/tendermint/tendermint/types" ) func TestBlockchainInfo(t *testing.T) { @@ -84,7 +84,10 @@ func TestBlockResults(t *testing.T) { env.StateStore = sm.NewStore(dbm.NewMemDB()) err := env.StateStore.SaveABCIResponses(100, results) require.NoError(t, err) - env.BlockStore = mockBlockStore{height: 100} + mockstore := &mocks.BlockStore{} + mockstore.On("Height").Return(int64(100)) + mockstore.On("Base").Return(int64(1)) + env.BlockStore = mockstore testCases := []struct { height int64 @@ -115,21 +118,3 @@ func TestBlockResults(t *testing.T) { } } } - -type mockBlockStore struct { - height int64 -} - -func (mockBlockStore) Base() int64 { return 1 } -func (store mockBlockStore) Height() int64 { return store.height } -func (store mockBlockStore) Size() int64 { return store.height } -func (mockBlockStore) LoadBaseMeta() *types.BlockMeta { return nil } -func (mockBlockStore) LoadBlockMeta(height int64) *types.BlockMeta { return nil } -func (mockBlockStore) LoadBlock(height int64) *types.Block { return nil } -func (mockBlockStore) LoadBlockByHash(hash []byte) *types.Block { return nil } -func (mockBlockStore) LoadBlockPart(height int64, index int) *types.Part { return nil } -func (mockBlockStore) LoadBlockCommit(height int64) *types.Commit { return nil } -func (mockBlockStore) LoadSeenCommit() *types.Commit { return nil } -func (mockBlockStore) PruneBlocks(height int64) (uint64, error) { return 0, nil } -func (mockBlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) { -} diff --git a/internal/rpc/core/routes.go b/internal/rpc/core/routes.go index 948e8f36d..740fe3b19 100644 --- a/internal/rpc/core/routes.go +++ b/internal/rpc/core/routes.go @@ -23,6 +23,8 @@ func (env *Environment) GetRoutes() RoutesMap { "blockchain": rpc.NewRPCFunc(env.BlockchainInfo, "minHeight,maxHeight", true), "genesis": rpc.NewRPCFunc(env.Genesis, "", true), "genesis_chunked": rpc.NewRPCFunc(env.GenesisChunked, "chunk", true), + "header": rpc.NewRPCFunc(env.Header, "height", true), + "header_by_hash": rpc.NewRPCFunc(env.HeaderByHash, "hash", true), "block": rpc.NewRPCFunc(env.Block, "height", true), "block_by_hash": rpc.NewRPCFunc(env.BlockByHash, "hash", true), "block_results": rpc.NewRPCFunc(env.BlockResults, "height", true), diff --git a/internal/state/mocks/block_store.go b/internal/state/mocks/block_store.go index e66aad071..563183437 100644 --- a/internal/state/mocks/block_store.go +++ b/internal/state/mocks/block_store.go @@ -121,6 +121,22 @@ func (_m *BlockStore) LoadBlockMeta(height int64) *types.BlockMeta { return r0 } +// LoadBlockMetaByHash provides a mock function with given fields: hash +func (_m *BlockStore) LoadBlockMetaByHash(hash []byte) *types.BlockMeta { + ret := _m.Called(hash) + + var r0 *types.BlockMeta + if rf, ok := ret.Get(0).(func([]byte) *types.BlockMeta); ok { + r0 = rf(hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.BlockMeta) + } + } + + return r0 +} + // LoadBlockPart provides a mock function with given fields: height, index func (_m *BlockStore) LoadBlockPart(height int64, index int) *types.Part { ret := _m.Called(height, index) diff --git a/internal/state/services.go b/internal/state/services.go index 49388cc12..2c9d312fb 100644 --- a/internal/state/services.go +++ b/internal/state/services.go @@ -29,6 +29,7 @@ type BlockStore interface { PruneBlocks(height int64) (uint64, error) LoadBlockByHash(hash []byte) *types.Block + LoadBlockMetaByHash(hash []byte) *types.BlockMeta LoadBlockPart(height int64, index int) *types.Part LoadBlockCommit(height int64) *types.Commit diff --git a/internal/store/store.go b/internal/store/store.go index c978241ff..6cdcdf719 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -181,6 +181,26 @@ func (bs *BlockStore) LoadBlockByHash(hash []byte) *types.Block { return bs.LoadBlock(height) } +// LoadBlockMetaByHash returns the blockmeta who's header corresponds to the given +// hash. If none is found, returns nil. +func (bs *BlockStore) LoadBlockMetaByHash(hash []byte) *types.BlockMeta { + bz, err := bs.db.Get(blockHashKey(hash)) + if err != nil { + panic(err) + } + if len(bz) == 0 { + return nil + } + + s := string(bz) + height, err := strconv.ParseInt(s, 10, 64) + + if err != nil { + panic(fmt.Sprintf("failed to extract height from %s: %v", s, err)) + } + return bs.LoadBlockMeta(height) +} + // LoadBlockPart returns the Part at the given index // from the block at the given height. // If no part is found for the given height and index, it returns nil. diff --git a/light/proxy/routes.go b/light/proxy/routes.go index 659cb5051..ac2e8b5df 100644 --- a/light/proxy/routes.go +++ b/light/proxy/routes.go @@ -24,6 +24,8 @@ func RPCRoutes(c *lrpc.Client) map[string]*rpcserver.RPCFunc { "blockchain": rpcserver.NewRPCFunc(makeBlockchainInfoFunc(c), "minHeight,maxHeight", true), "genesis": rpcserver.NewRPCFunc(makeGenesisFunc(c), "", true), "genesis_chunked": rpcserver.NewRPCFunc(makeGenesisChunkedFunc(c), "", true), + "header": rpcserver.NewRPCFunc(makeHeaderFunc(c), "height", true), + "header_by_hash": rpcserver.NewRPCFunc(makeHeaderByHashFunc(c), "hash", true), "block": rpcserver.NewRPCFunc(makeBlockFunc(c), "height", true), "block_by_hash": rpcserver.NewRPCFunc(makeBlockByHashFunc(c), "hash", true), "block_results": rpcserver.NewRPCFunc(makeBlockResultsFunc(c), "height", true), @@ -101,6 +103,22 @@ func makeGenesisChunkedFunc(c *lrpc.Client) rpcGenesisChunkedFunc { } } +type rpcHeaderFunc func(ctx *rpctypes.Context, height *int64) (*coretypes.ResultHeader, error) + +func makeHeaderFunc(c *lrpc.Client) rpcHeaderFunc { + return func(ctx *rpctypes.Context, height *int64) (*coretypes.ResultHeader, error) { + return c.Header(ctx.Context(), height) + } +} + +type rpcHeaderByHashFunc func(ctx *rpctypes.Context, hash []byte) (*coretypes.ResultHeader, error) + +func makeHeaderByHashFunc(c *lrpc.Client) rpcHeaderByHashFunc { + return func(ctx *rpctypes.Context, hash []byte) (*coretypes.ResultHeader, error) { + return c.HeaderByHash(ctx.Context(), hash) + } +} + type rpcBlockFunc func(ctx *rpctypes.Context, height *int64) (*coretypes.ResultBlock, error) func makeBlockFunc(c *lrpc.Client) rpcBlockFunc { diff --git a/light/rpc/client.go b/light/rpc/client.go index d03bb510d..9101de28b 100644 --- a/light/rpc/client.go +++ b/light/rpc/client.go @@ -442,6 +442,40 @@ func (c *Client) BlockResults(ctx context.Context, height *int64) (*coretypes.Re return res, nil } +// Header fetches and verifies the header directly via the light client +func (c *Client) Header(ctx context.Context, height *int64) (*coretypes.ResultHeader, error) { + lb, err := c.updateLightClientIfNeededTo(ctx, height) + if err != nil { + return nil, err + } + + return &coretypes.ResultHeader{Header: lb.Header}, nil +} + +// HeaderByHash calls rpcclient#HeaderByHash and updates the client if it's falling behind. +func (c *Client) HeaderByHash(ctx context.Context, hash tmbytes.HexBytes) (*coretypes.ResultHeader, error) { + res, err := c.next.HeaderByHash(ctx, hash) + if err != nil { + return nil, err + } + + if err := res.Header.ValidateBasic(); err != nil { + return nil, err + } + + lb, err := c.updateLightClientIfNeededTo(ctx, &res.Header.Height) + if err != nil { + return nil, err + } + + if !bytes.Equal(lb.Header.Hash(), res.Header.Hash()) { + return nil, fmt.Errorf("primary header hash does not match trusted header hash. (%X != %X)", + lb.Header.Hash(), res.Header.Hash()) + } + + return res, nil +} + func (c *Client) Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) { // Update the light client if we're behind and retrieve the light block at the requested height // or at the latest height if no height is provided. @@ -526,7 +560,8 @@ func (c *Client) Validators( BlockHeight: l.Height, Validators: v, Count: len(v), - Total: totalCount}, nil + Total: totalCount, + }, nil } func (c *Client) BroadcastEvidence(ctx context.Context, ev types.Evidence) (*coretypes.ResultBroadcastEvidence, error) { diff --git a/rpc/client/http/http.go b/rpc/client/http/http.go index d0c1d5621..6c518e899 100644 --- a/rpc/client/http/http.go +++ b/rpc/client/http/http.go @@ -91,9 +91,11 @@ type baseRPCClient struct { caller jsonrpcclient.Caller } -var _ rpcClient = (*HTTP)(nil) -var _ rpcClient = (*BatchHTTP)(nil) -var _ rpcClient = (*baseRPCClient)(nil) +var ( + _ rpcClient = (*HTTP)(nil) + _ rpcClient = (*BatchHTTP)(nil) + _ rpcClient = (*baseRPCClient)(nil) +) //----------------------------------------------------------------------------- // HTTP @@ -449,6 +451,31 @@ func (c *baseRPCClient) BlockResults( return result, nil } +func (c *baseRPCClient) Header(ctx context.Context, height *int64) (*coretypes.ResultHeader, error) { + result := new(coretypes.ResultHeader) + params := make(map[string]interface{}) + if height != nil { + params["height"] = height + } + _, err := c.caller.Call(ctx, "header", params, result) + if err != nil { + return nil, err + } + return result, nil +} + +func (c *baseRPCClient) HeaderByHash(ctx context.Context, hash bytes.HexBytes) (*coretypes.ResultHeader, error) { + result := new(coretypes.ResultHeader) + params := map[string]interface{}{ + "hash": hash, + } + _, err := c.caller.Call(ctx, "header_by_hash", params, result) + if err != nil { + return nil, err + } + return result, nil +} + func (c *baseRPCClient) Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) { result := new(coretypes.ResultCommit) params := make(map[string]interface{}) diff --git a/rpc/client/interface.go b/rpc/client/interface.go index 420b6fe52..5f6d9addd 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -78,6 +78,8 @@ type SignClient interface { Block(ctx context.Context, height *int64) (*coretypes.ResultBlock, error) BlockByHash(ctx context.Context, hash bytes.HexBytes) (*coretypes.ResultBlock, error) BlockResults(ctx context.Context, height *int64) (*coretypes.ResultBlockResults, error) + Header(ctx context.Context, height *int64) (*coretypes.ResultHeader, error) + HeaderByHash(ctx context.Context, hash bytes.HexBytes) (*coretypes.ResultHeader, error) Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) Validators(ctx context.Context, height *int64, page, perPage *int) (*coretypes.ResultValidators, error) Tx(ctx context.Context, hash bytes.HexBytes, prove bool) (*coretypes.ResultTx, error) diff --git a/rpc/client/local/local.go b/rpc/client/local/local.go index c4f2cbc95..95428a93d 100644 --- a/rpc/client/local/local.go +++ b/rpc/client/local/local.go @@ -178,6 +178,14 @@ func (c *Local) BlockResults(ctx context.Context, height *int64) (*coretypes.Res return c.env.BlockResults(c.ctx, height) } +func (c *Local) Header(ctx context.Context, height *int64) (*coretypes.ResultHeader, error) { + return c.env.Header(c.ctx, height) +} + +func (c *Local) HeaderByHash(ctx context.Context, hash bytes.HexBytes) (*coretypes.ResultHeader, error) { + return c.env.HeaderByHash(c.ctx, hash) +} + func (c *Local) Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) { return c.env.Commit(c.ctx, height) } diff --git a/rpc/client/mocks/client.go b/rpc/client/mocks/client.go new file mode 100644 index 000000000..3c3ebd443 --- /dev/null +++ b/rpc/client/mocks/client.go @@ -0,0 +1,802 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + bytes "github.com/tendermint/tendermint/libs/bytes" + client "github.com/tendermint/tendermint/rpc/client" + + context "context" + + coretypes "github.com/tendermint/tendermint/rpc/coretypes" + + mock "github.com/stretchr/testify/mock" + + types "github.com/tendermint/tendermint/types" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// ABCIInfo provides a mock function with given fields: _a0 +func (_m *Client) ABCIInfo(_a0 context.Context) (*coretypes.ResultABCIInfo, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultABCIInfo + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultABCIInfo); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultABCIInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ABCIQuery provides a mock function with given fields: ctx, path, data +func (_m *Client) ABCIQuery(ctx context.Context, path string, data bytes.HexBytes) (*coretypes.ResultABCIQuery, error) { + ret := _m.Called(ctx, path, data) + + var r0 *coretypes.ResultABCIQuery + if rf, ok := ret.Get(0).(func(context.Context, string, bytes.HexBytes) *coretypes.ResultABCIQuery); ok { + r0 = rf(ctx, path, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultABCIQuery) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, bytes.HexBytes) error); ok { + r1 = rf(ctx, path, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ABCIQueryWithOptions provides a mock function with given fields: ctx, path, data, opts +func (_m *Client) ABCIQueryWithOptions(ctx context.Context, path string, data bytes.HexBytes, opts client.ABCIQueryOptions) (*coretypes.ResultABCIQuery, error) { + ret := _m.Called(ctx, path, data, opts) + + var r0 *coretypes.ResultABCIQuery + if rf, ok := ret.Get(0).(func(context.Context, string, bytes.HexBytes, client.ABCIQueryOptions) *coretypes.ResultABCIQuery); ok { + r0 = rf(ctx, path, data, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultABCIQuery) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, bytes.HexBytes, client.ABCIQueryOptions) error); ok { + r1 = rf(ctx, path, data, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Block provides a mock function with given fields: ctx, height +func (_m *Client) Block(ctx context.Context, height *int64) (*coretypes.ResultBlock, error) { + ret := _m.Called(ctx, height) + + var r0 *coretypes.ResultBlock + if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultBlock); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBlock) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BlockByHash provides a mock function with given fields: ctx, hash +func (_m *Client) BlockByHash(ctx context.Context, hash bytes.HexBytes) (*coretypes.ResultBlock, error) { + ret := _m.Called(ctx, hash) + + var r0 *coretypes.ResultBlock + if rf, ok := ret.Get(0).(func(context.Context, bytes.HexBytes) *coretypes.ResultBlock); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBlock) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, bytes.HexBytes) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BlockResults provides a mock function with given fields: ctx, height +func (_m *Client) BlockResults(ctx context.Context, height *int64) (*coretypes.ResultBlockResults, error) { + ret := _m.Called(ctx, height) + + var r0 *coretypes.ResultBlockResults + if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultBlockResults); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBlockResults) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BlockSearch provides a mock function with given fields: ctx, query, page, perPage, orderBy +func (_m *Client) BlockSearch(ctx context.Context, query string, page *int, perPage *int, orderBy string) (*coretypes.ResultBlockSearch, error) { + ret := _m.Called(ctx, query, page, perPage, orderBy) + + var r0 *coretypes.ResultBlockSearch + if rf, ok := ret.Get(0).(func(context.Context, string, *int, *int, string) *coretypes.ResultBlockSearch); ok { + r0 = rf(ctx, query, page, perPage, orderBy) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBlockSearch) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *int, *int, string) error); ok { + r1 = rf(ctx, query, page, perPage, orderBy) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BlockchainInfo provides a mock function with given fields: ctx, minHeight, maxHeight +func (_m *Client) BlockchainInfo(ctx context.Context, minHeight int64, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) { + ret := _m.Called(ctx, minHeight, maxHeight) + + var r0 *coretypes.ResultBlockchainInfo + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *coretypes.ResultBlockchainInfo); ok { + r0 = rf(ctx, minHeight, maxHeight) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBlockchainInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok { + r1 = rf(ctx, minHeight, maxHeight) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BroadcastEvidence provides a mock function with given fields: _a0, _a1 +func (_m *Client) BroadcastEvidence(_a0 context.Context, _a1 types.Evidence) (*coretypes.ResultBroadcastEvidence, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultBroadcastEvidence + if rf, ok := ret.Get(0).(func(context.Context, types.Evidence) *coretypes.ResultBroadcastEvidence); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBroadcastEvidence) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, types.Evidence) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BroadcastTxAsync provides a mock function with given fields: _a0, _a1 +func (_m *Client) BroadcastTxAsync(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultBroadcastTx, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultBroadcastTx + if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultBroadcastTx); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBroadcastTx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BroadcastTxCommit provides a mock function with given fields: _a0, _a1 +func (_m *Client) BroadcastTxCommit(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultBroadcastTxCommit, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultBroadcastTxCommit + if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultBroadcastTxCommit); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBroadcastTxCommit) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BroadcastTxSync provides a mock function with given fields: _a0, _a1 +func (_m *Client) BroadcastTxSync(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultBroadcastTx, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultBroadcastTx + if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultBroadcastTx); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBroadcastTx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CheckTx provides a mock function with given fields: _a0, _a1 +func (_m *Client) CheckTx(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultCheckTx, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultCheckTx + if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultCheckTx); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultCheckTx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Commit provides a mock function with given fields: ctx, height +func (_m *Client) Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) { + ret := _m.Called(ctx, height) + + var r0 *coretypes.ResultCommit + if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultCommit); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultCommit) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsensusParams provides a mock function with given fields: ctx, height +func (_m *Client) ConsensusParams(ctx context.Context, height *int64) (*coretypes.ResultConsensusParams, error) { + ret := _m.Called(ctx, height) + + var r0 *coretypes.ResultConsensusParams + if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultConsensusParams); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultConsensusParams) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConsensusState provides a mock function with given fields: _a0 +func (_m *Client) ConsensusState(_a0 context.Context) (*coretypes.ResultConsensusState, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultConsensusState + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultConsensusState); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultConsensusState) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DumpConsensusState provides a mock function with given fields: _a0 +func (_m *Client) DumpConsensusState(_a0 context.Context) (*coretypes.ResultDumpConsensusState, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultDumpConsensusState + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultDumpConsensusState); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultDumpConsensusState) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Genesis provides a mock function with given fields: _a0 +func (_m *Client) Genesis(_a0 context.Context) (*coretypes.ResultGenesis, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultGenesis + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultGenesis); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultGenesis) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GenesisChunked provides a mock function with given fields: _a0, _a1 +func (_m *Client) GenesisChunked(_a0 context.Context, _a1 uint) (*coretypes.ResultGenesisChunk, error) { + ret := _m.Called(_a0, _a1) + + var r0 *coretypes.ResultGenesisChunk + if rf, ok := ret.Get(0).(func(context.Context, uint) *coretypes.ResultGenesisChunk); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultGenesisChunk) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, uint) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Header provides a mock function with given fields: ctx, height +func (_m *Client) Header(ctx context.Context, height *int64) (*coretypes.ResultHeader, error) { + ret := _m.Called(ctx, height) + + var r0 *coretypes.ResultHeader + if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultHeader); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultHeader) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// HeaderByHash provides a mock function with given fields: ctx, hash +func (_m *Client) HeaderByHash(ctx context.Context, hash bytes.HexBytes) (*coretypes.ResultHeader, error) { + ret := _m.Called(ctx, hash) + + var r0 *coretypes.ResultHeader + if rf, ok := ret.Get(0).(func(context.Context, bytes.HexBytes) *coretypes.ResultHeader); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultHeader) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, bytes.HexBytes) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Health provides a mock function with given fields: _a0 +func (_m *Client) Health(_a0 context.Context) (*coretypes.ResultHealth, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultHealth + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultHealth); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultHealth) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsRunning provides a mock function with given fields: +func (_m *Client) IsRunning() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// NetInfo provides a mock function with given fields: _a0 +func (_m *Client) NetInfo(_a0 context.Context) (*coretypes.ResultNetInfo, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultNetInfo + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultNetInfo); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultNetInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NumUnconfirmedTxs provides a mock function with given fields: _a0 +func (_m *Client) NumUnconfirmedTxs(_a0 context.Context) (*coretypes.ResultUnconfirmedTxs, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultUnconfirmedTxs + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultUnconfirmedTxs); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultUnconfirmedTxs) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveTx provides a mock function with given fields: _a0, _a1 +func (_m *Client) RemoveTx(_a0 context.Context, _a1 types.TxKey) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, types.TxKey) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Start provides a mock function with given fields: _a0 +func (_m *Client) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Status provides a mock function with given fields: _a0 +func (_m *Client) Status(_a0 context.Context) (*coretypes.ResultStatus, error) { + ret := _m.Called(_a0) + + var r0 *coretypes.ResultStatus + if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultStatus); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultStatus) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Stop provides a mock function with given fields: +func (_m *Client) Stop() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Subscribe provides a mock function with given fields: ctx, subscriber, query, outCapacity +func (_m *Client) Subscribe(ctx context.Context, subscriber string, query string, outCapacity ...int) (<-chan coretypes.ResultEvent, error) { + _va := make([]interface{}, len(outCapacity)) + for _i := range outCapacity { + _va[_i] = outCapacity[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, subscriber, query) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 <-chan coretypes.ResultEvent + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...int) <-chan coretypes.ResultEvent); ok { + r0 = rf(ctx, subscriber, query, outCapacity...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan coretypes.ResultEvent) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string, ...int) error); ok { + r1 = rf(ctx, subscriber, query, outCapacity...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Tx provides a mock function with given fields: ctx, hash, prove +func (_m *Client) Tx(ctx context.Context, hash bytes.HexBytes, prove bool) (*coretypes.ResultTx, error) { + ret := _m.Called(ctx, hash, prove) + + var r0 *coretypes.ResultTx + if rf, ok := ret.Get(0).(func(context.Context, bytes.HexBytes, bool) *coretypes.ResultTx); ok { + r0 = rf(ctx, hash, prove) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultTx) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, bytes.HexBytes, bool) error); ok { + r1 = rf(ctx, hash, prove) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TxSearch provides a mock function with given fields: ctx, query, prove, page, perPage, orderBy +func (_m *Client) TxSearch(ctx context.Context, query string, prove bool, page *int, perPage *int, orderBy string) (*coretypes.ResultTxSearch, error) { + ret := _m.Called(ctx, query, prove, page, perPage, orderBy) + + var r0 *coretypes.ResultTxSearch + if rf, ok := ret.Get(0).(func(context.Context, string, bool, *int, *int, string) *coretypes.ResultTxSearch); ok { + r0 = rf(ctx, query, prove, page, perPage, orderBy) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultTxSearch) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, bool, *int, *int, string) error); ok { + r1 = rf(ctx, query, prove, page, perPage, orderBy) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnconfirmedTxs provides a mock function with given fields: ctx, limit +func (_m *Client) UnconfirmedTxs(ctx context.Context, limit *int) (*coretypes.ResultUnconfirmedTxs, error) { + ret := _m.Called(ctx, limit) + + var r0 *coretypes.ResultUnconfirmedTxs + if rf, ok := ret.Get(0).(func(context.Context, *int) *coretypes.ResultUnconfirmedTxs); ok { + r0 = rf(ctx, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultUnconfirmedTxs) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int) error); ok { + r1 = rf(ctx, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Unsubscribe provides a mock function with given fields: ctx, subscriber, query +func (_m *Client) Unsubscribe(ctx context.Context, subscriber string, query string) error { + ret := _m.Called(ctx, subscriber, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, subscriber, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UnsubscribeAll provides a mock function with given fields: ctx, subscriber +func (_m *Client) UnsubscribeAll(ctx context.Context, subscriber string) error { + ret := _m.Called(ctx, subscriber) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, subscriber) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Validators provides a mock function with given fields: ctx, height, page, perPage +func (_m *Client) Validators(ctx context.Context, height *int64, page *int, perPage *int) (*coretypes.ResultValidators, error) { + ret := _m.Called(ctx, height, page, perPage) + + var r0 *coretypes.ResultValidators + if rf, ok := ret.Get(0).(func(context.Context, *int64, *int, *int) *coretypes.ResultValidators); ok { + r0 = rf(ctx, height, page, perPage) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultValidators) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *int64, *int, *int) error); ok { + r1 = rf(ctx, height, page, perPage) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 8a6845831..5c980bdd6 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -340,6 +340,15 @@ func TestAppCalls(t *testing.T) { require.NoError(t, err) require.Equal(t, block, blockByHash) + // check that the header matches the block hash + header, err := c.Header(ctx, &apph) + require.NoError(t, err) + require.Equal(t, block.Block.Header, *header.Header) + + headerByHash, err := c.HeaderByHash(ctx, block.BlockID.Hash) + require.NoError(t, err) + require.Equal(t, header, headerByHash) + // now check the results blockResults, err := c.BlockResults(ctx, &txh) require.NoError(t, err, "%d: %+v", i, err) diff --git a/rpc/coretypes/responses.go b/rpc/coretypes/responses.go index ecb058312..223a25ff7 100644 --- a/rpc/coretypes/responses.go +++ b/rpc/coretypes/responses.go @@ -51,6 +51,11 @@ type ResultBlock struct { Block *types.Block `json:"block"` } +// ResultHeader represents the response for a Header RPC Client query +type ResultHeader struct { + Header *types.Header `json:"header"` +} + // Commit and Header type ResultCommit struct { types.SignedHeader `json:"signed_header"` diff --git a/rpc/openapi/openapi.yaml b/rpc/openapi/openapi.yaml index 9e4f79dfa..ad3ccbb60 100644 --- a/rpc/openapi/openapi.yaml +++ b/rpc/openapi/openapi.yaml @@ -525,7 +525,7 @@ paths: $ref: "#/components/schemas/ErrorResponse" /net_info: get: - summary: Network informations + summary: Network information operationId: net_info tags: - Info @@ -693,6 +693,64 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorResponse" + /header: + get: + summary: Get the header at a specified height + operationId: header + parameters: + - in: query + name: height + schema: + type: integer + default: 0 + example: 1 + description: height to return. If no height is provided, it will fetch the latest height. + tags: + - Info + description: | + Retrieve the block header corresponding to a specified height. + responses: + "200": + description: Header information. + content: + application/json: + schema: + $ref: "#/components/schemas/HeaderResponse" + "500": + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /header_by_hash: + get: + summary: Get header by hash + operationId: header_by_hash + parameters: + - in: query + name: hash + description: header hash + required: true + schema: + type: string + example: "0xD70952032620CC4E2737EB8AC379806359D8E0B17B0488F627997A0B043ABDED" + tags: + - Info + description: | + Retrieve the block header corresponding to a block hash. + responses: + "200": + description: Header information. + content: + application/json: + schema: + $ref: "#/components/schemas/HeaderResponse" + "500": + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /block: get: summary: Get block at a specified height @@ -711,7 +769,7 @@ paths: Get Block. responses: "200": - description: Block informations. + description: Block information. content: application/json: schema: @@ -740,7 +798,7 @@ paths: Get Block By Hash. responses: "200": - description: Block informations. + description: Block information. content: application/json: schema: @@ -758,7 +816,7 @@ paths: parameters: - in: query name: height - description: height to return. If no height is provided, it will fetch informations regarding the latest block. + description: height to return. If no height is provided, it will fetch information regarding the latest block. schema: type: integer default: 0 @@ -787,7 +845,7 @@ paths: parameters: - in: query name: height - description: height to return. If no height is provided, it will fetch commit informations regarding the latest block. + description: height to return. If no height is provided, it will fetch commit information regarding the latest block. schema: type: integer default: 0 @@ -968,7 +1026,7 @@ paths: parameters: - in: query name: height - description: height to return. If no height is provided, it will fetch commit informations regarding the latest block. + description: height to return. If no height is provided, it will fetch commit information regarding the latest block. schema: type: integer default: 0 @@ -1703,13 +1761,21 @@ components: block: $ref: "#/components/schemas/Block" BlockResponse: - description: Blockc info + description: Block info allOf: - $ref: "#/components/schemas/JSONRPC" - type: object properties: result: $ref: "#/components/schemas/BlockComplete" + HeaderResponse: + description: Block Header info + allOf: + - $ref: "#/components/schemas/JSONRPC" + - type: object + properties: + result: + $ref: "#/components/schemas/BlockHeader" ################## FROM NOW ON NEEDS REFACTOR ################## BlockResultsResponse: