From 8b45f2907f11d7ef76a148390606b68d426b1207 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 3 Sep 2022 08:54:25 -0400 Subject: [PATCH] Update rpc client header (#9276) (#9349) * Update rpc client header (cherry picked from commit 2ff11e5bc277db701b52ee07e74e81a2211150c4) Co-authored-by: samricotta <37125168+samricotta@users.noreply.github.com> --- CHANGELOG_PENDING.md | 1 + consensus/replay_test.go | 1 + light/proxy/routes.go | 18 +++++++++++ light/rpc/client.go | 34 +++++++++++++++++++++ rpc/client/http/http.go | 33 ++++++++++++++++++-- rpc/client/interface.go | 2 ++ rpc/client/local/local.go | 8 +++++ rpc/client/mocks/client.go | 45 ++++++++++++++++++++++++++++ rpc/client/rpc_test.go | 9 ++++++ rpc/core/blocks.go | 33 ++++++++++++++++++++ rpc/core/blocks_test.go | 25 ++++------------ rpc/core/routes.go | 2 ++ rpc/core/types/responses.go | 5 ++++ rpc/openapi/openapi.yaml | 60 ++++++++++++++++++++++++++++++++++++- state/mocks/block_store.go | 16 ++++++++++ state/services.go | 1 + store/store.go | 20 +++++++++++++ store/store_test.go | 23 ++++++++++++++ 18 files changed, 312 insertions(+), 24 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index adf68a800..5b217cfa5 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -36,6 +36,7 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi ### IMPROVEMENTS +- [rpc] \#9276 Added `header` and `header_by_hash` queries to the RPC client (@samricotta) - [abci] \#5706 Added `AbciVersion` to `RequestInfo` allowing applications to check ABCI version when connecting to Tendermint. (@marbar3778) ### BUG FIXES diff --git a/consensus/replay_test.go b/consensus/replay_test.go index cfc23898e..e3b8775da 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -1164,6 +1164,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] bps, err := block.MakePartSet(types.BlockPartSizeBytes) diff --git a/light/proxy/routes.go b/light/proxy/routes.go index a641268eb..c97a91dfd 100644 --- a/light/proxy/routes.go +++ b/light/proxy/routes.go @@ -25,6 +25,8 @@ func RPCRoutes(c *lrpc.Client) map[string]*rpcserver.RPCFunc { "genesis": rpcserver.NewRPCFunc(makeGenesisFunc(c), ""), "genesis_chunked": rpcserver.NewRPCFunc(makeGenesisChunkedFunc(c), ""), "block": rpcserver.NewRPCFunc(makeBlockFunc(c), "height"), + "header": rpcserver.NewRPCFunc(makeHeaderFunc(c), "height"), + "header_by_hash": rpcserver.NewRPCFunc(makeHeaderByHashFunc(c), "hash"), "block_by_hash": rpcserver.NewRPCFunc(makeBlockByHashFunc(c), "hash"), "block_results": rpcserver.NewRPCFunc(makeBlockResultsFunc(c), "height"), "commit": rpcserver.NewRPCFunc(makeCommitFunc(c), "height"), @@ -108,6 +110,22 @@ func makeBlockFunc(c *lrpc.Client) rpcBlockFunc { } } +type rpcHeaderFunc func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultHeader, error) + +func makeHeaderFunc(c *lrpc.Client) rpcHeaderFunc { + return func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultHeader, error) { + return c.Header(ctx.Context(), height) + } +} + +type rpcHeaderByHashFunc func(ctx *rpctypes.Context, hash []byte) (*ctypes.ResultHeader, error) + +func makeHeaderByHashFunc(c *lrpc.Client) rpcHeaderByHashFunc { + return func(ctx *rpctypes.Context, hash []byte) (*ctypes.ResultHeader, error) { + return c.HeaderByHash(ctx.Context(), hash) + } +} + type rpcBlockByHashFunc func(ctx *rpctypes.Context, hash []byte) (*ctypes.ResultBlock, error) func makeBlockByHashFunc(c *lrpc.Client) rpcBlockByHashFunc { diff --git a/light/rpc/client.go b/light/rpc/client.go index 0639d20eb..8eaf40a55 100644 --- a/light/rpc/client.go +++ b/light/rpc/client.go @@ -441,6 +441,40 @@ func (c *Client) BlockResults(ctx context.Context, height *int64) (*ctypes.Resul return res, nil } +// Header fetches and verifies the header directly via the light client +func (c *Client) Header(ctx context.Context, height *int64) (*ctypes.ResultHeader, error) { + lb, err := c.updateLightClientIfNeededTo(ctx, height) + if err != nil { + return nil, err + } + + return &ctypes.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) (*ctypes.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) (*ctypes.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. diff --git a/rpc/client/http/http.go b/rpc/client/http/http.go index 4fec87f2f..8af6b3ee7 100644 --- a/rpc/client/http/http.go +++ b/rpc/client/http/http.go @@ -98,9 +98,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 @@ -444,6 +446,31 @@ func (c *baseRPCClient) BlockResults( return result, nil } +func (c *baseRPCClient) Header(ctx context.Context, height *int64) (*ctypes.ResultHeader, error) { + result := new(ctypes.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) (*ctypes.ResultHeader, error) { + result := new(ctypes.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) (*ctypes.ResultCommit, error) { result := new(ctypes.ResultCommit) params := make(map[string]interface{}) diff --git a/rpc/client/interface.go b/rpc/client/interface.go index 36dc2f7d1..92783634c 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -67,6 +67,8 @@ type SignClient interface { Block(ctx context.Context, height *int64) (*ctypes.ResultBlock, error) BlockByHash(ctx context.Context, hash []byte) (*ctypes.ResultBlock, error) BlockResults(ctx context.Context, height *int64) (*ctypes.ResultBlockResults, error) + Header(ctx context.Context, height *int64) (*ctypes.ResultHeader, error) + HeaderByHash(ctx context.Context, hash bytes.HexBytes) (*ctypes.ResultHeader, error) Commit(ctx context.Context, height *int64) (*ctypes.ResultCommit, error) Validators(ctx context.Context, height *int64, page, perPage *int) (*ctypes.ResultValidators, error) Tx(ctx context.Context, hash []byte, prove bool) (*ctypes.ResultTx, error) diff --git a/rpc/client/local/local.go b/rpc/client/local/local.go index d0dfc3723..ca324dee0 100644 --- a/rpc/client/local/local.go +++ b/rpc/client/local/local.go @@ -169,6 +169,14 @@ func (c *Local) BlockResults(ctx context.Context, height *int64) (*ctypes.Result return core.BlockResults(c.ctx, height) } +func (c *Local) Header(ctx context.Context, height *int64) (*ctypes.ResultHeader, error) { + return core.Header(c.ctx, height) +} + +func (c *Local) HeaderByHash(ctx context.Context, hash bytes.HexBytes) (*ctypes.ResultHeader, error) { + return core.HeaderByHash(c.ctx, hash) +} + func (c *Local) Commit(ctx context.Context, height *int64) (*ctypes.ResultCommit, error) { return core.Commit(c.ctx, height) } diff --git a/rpc/client/mocks/client.go b/rpc/client/mocks/client.go index f8eb7a45c..3569d54d6 100644 --- a/rpc/client/mocks/client.go +++ b/rpc/client/mocks/client.go @@ -458,6 +458,51 @@ func (_m *Client) GenesisChunked(_a0 context.Context, _a1 uint) (*coretypes.Resu 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) { diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 79102a364..4b8b11f98 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -285,6 +285,15 @@ func TestAppCalls(t *testing.T) { require.NoError(err) require.Equal(block, blockByHash) + // check that the header matches the block hash + header, err := c.Header(context.Background(), &apph) + require.NoError(err) + require.Equal(block.Block.Header, *header.Header) + + headerByHash, err := c.HeaderByHash(context.Background(), block.BlockID.Hash) + require.NoError(err) + require.Equal(header, headerByHash) + // now check the results blockResults, err := c.BlockResults(context.Background(), &txh) require.Nil(err, "%d: %+v", i, err) diff --git a/rpc/core/blocks.go b/rpc/core/blocks.go index 372ac60da..bad6f0360 100644 --- a/rpc/core/blocks.go +++ b/rpc/core/blocks.go @@ -5,6 +5,7 @@ import ( "fmt" "sort" + "github.com/tendermint/tendermint/libs/bytes" tmmath "github.com/tendermint/tendermint/libs/math" tmquery "github.com/tendermint/tendermint/libs/pubsub/query" ctypes "github.com/tendermint/tendermint/rpc/core/types" @@ -76,6 +77,38 @@ func filterMinMax(base, height, min, max, limit int64) (int64, int64, error) { return min, max, 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 Header(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultHeader, error) { + height, err := getHeight(env.BlockStore.Height(), heightPtr) + if err != nil { + return nil, err + } + + blockMeta := env.BlockStore.LoadBlockMeta(height) + if blockMeta == nil { + return &ctypes.ResultHeader{}, nil + } + + return &ctypes.ResultHeader{Header: &blockMeta.Header}, nil +} + +// HeaderByHash gets header by hash. +// More: https://docs.tendermint.com/master/rpc/#/Info/header_by_hash +func HeaderByHash(ctx *rpctypes.Context, hash bytes.HexBytes) (*ctypes.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 &ctypes.ResultHeader{}, nil + } + + return &ctypes.ResultHeader{Header: &blockMeta.Header}, nil +} + // Block gets block at a given height. // If no height is provided, it will fetch the latest block. // More: https://docs.tendermint.com/v0.37/rpc/#/Info/block diff --git a/rpc/core/blocks_test.go b/rpc/core/blocks_test.go index 80d0643d2..71311076c 100644 --- a/rpc/core/blocks_test.go +++ b/rpc/core/blocks_test.go @@ -14,7 +14,7 @@ import ( ctypes "github.com/tendermint/tendermint/rpc/core/types" rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types" sm "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/types" + "github.com/tendermint/tendermint/state/mocks" ) func TestBlockchainInfo(t *testing.T) { @@ -86,7 +86,10 @@ func TestBlockResults(t *testing.T) { }) 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 @@ -116,21 +119,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(height int64) *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/rpc/core/routes.go b/rpc/core/routes.go index 195c6089a..fe2d17e8b 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -24,6 +24,8 @@ var Routes = map[string]*rpc.RPCFunc{ "block_by_hash": rpc.NewRPCFunc(BlockByHash, "hash"), "block_results": rpc.NewRPCFunc(BlockResults, "height"), "commit": rpc.NewRPCFunc(Commit, "height"), + "header": rpc.NewRPCFunc(Header, "height"), + "header_by_hash": rpc.NewRPCFunc(HeaderByHash, "hash"), "check_tx": rpc.NewRPCFunc(CheckTx, "tx"), "tx": rpc.NewRPCFunc(Tx, "hash,prove"), "tx_search": rpc.NewRPCFunc(TxSearch, "query,prove,page,per_page,order_by"), diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index 747f626a7..6da818890 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -39,6 +39,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 a0f51325c..bcdd50473 100644 --- a/rpc/openapi/openapi.yaml +++ b/rpc/openapi/openapi.yaml @@ -500,7 +500,7 @@ paths: $ref: "#/components/schemas/ErrorResponse" /net_info: get: - summary: Network informations + summary: Network information operationId: net_info tags: - Info @@ -637,6 +637,64 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorResponse" + /header: + get: + summary: Get 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 header. + tags: + - Info + description: | + Get Header. + responses: + "200": + description: Header informations. + content: + application/json: + schema: + $ref: "#/components/schemas/BlockHeader" + "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: | + Get Header By Hash. + responses: + "200": + description: Header informations. + content: + application/json: + schema: + $ref: "#/components/schemas/BlockHeader" + "500": + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /block: get: summary: Get block at a specified height diff --git a/state/mocks/block_store.go b/state/mocks/block_store.go index 4493a6e3f..f93f45447 100644 --- a/state/mocks/block_store.go +++ b/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/state/services.go b/state/services.go index 2b6c16fed..6e24af036 100644 --- a/state/services.go +++ b/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/store/store.go b/store/store.go index 48fd1c97e..312317021 100644 --- a/store/store.go +++ b/store/store.go @@ -196,6 +196,26 @@ func (bs *BlockStore) LoadBlockMeta(height int64) *types.BlockMeta { return blockMeta } +// 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(calcBlockHashKey(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) +} + // LoadBlockCommit returns the Commit for the given height. // This commit consists of the +2/3 and other Precommit-votes for block at `height`, // and it comes from the block.LastCommit for `height+1`. diff --git a/store/store_test.go b/store/store_test.go index 10d9b0af3..a7dcb7cf5 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -476,6 +476,7 @@ func TestPruneBlocks(t *testing.T) { require.Nil(t, bs.LoadBlockByHash(prunedBlock.Hash())) require.Nil(t, bs.LoadBlockCommit(1199)) require.Nil(t, bs.LoadBlockMeta(1199)) + require.Nil(t, bs.LoadBlockMetaByHash(prunedBlock.Hash())) require.Nil(t, bs.LoadBlockPart(1199, 1)) for i := int64(1); i < 1200; i++ { @@ -552,6 +553,28 @@ func TestLoadBlockMeta(t *testing.T) { } } +func TestLoadBlockMetaByHash(t *testing.T) { + config := cfg.ResetTestRoot("blockchain_reactor_test") + defer os.RemoveAll(config.RootDir) + stateStore := sm.NewStore(dbm.NewMemDB(), sm.StoreOptions{ + DiscardABCIResponses: false, + }) + state, err := stateStore.LoadFromDBOrGenesisFile(config.GenesisFile()) + require.NoError(t, err) + bs := NewBlockStore(dbm.NewMemDB()) + + b1 := state.MakeBlock(state.LastBlockHeight+1, test.MakeNTxs(state.LastBlockHeight+1, 10), new(types.Commit), nil, state.Validators.GetProposer().Address) + partSet, err := b1.MakePartSet(2) + require.NoError(t, err) + seenCommit := makeTestCommit(1, tmtime.Now()) + bs.SaveBlock(b1, partSet, seenCommit) + + baseBlock := bs.LoadBlockMetaByHash(b1.Hash()) + assert.EqualValues(t, b1.Header.Height, baseBlock.Header.Height) + assert.EqualValues(t, b1.Header.LastBlockID, baseBlock.Header.LastBlockID) + assert.EqualValues(t, b1.Header.ChainID, baseBlock.Header.ChainID) +} + func TestBlockFetchAtHeight(t *testing.T) { state, bs, cleanup := makeStateAndBlockStore(log.NewTMLogger(new(bytes.Buffer))) defer cleanup()