diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 84abf5b00..9f39edaa6 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -39,6 +39,7 @@ 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) - [cli] [#7033](https://github.com/tendermint/tendermint/pull/7033) Add a `rollback` command to rollback to the previous tendermint state in the event of non-determinstic app hash or reverting an upgrade. - [mempool, rpc] \#7041 Add removeTx operation to the RPC layer. (@tychoish) diff --git a/internal/consensus/replay_test.go b/internal/consensus/replay_test.go index 6c33268a7..036614b71 100644 --- a/internal/consensus/replay_test.go +++ b/internal/consensus/replay_test.go @@ -1241,6 +1241,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 fe99d2118..dd53711c3 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 a5317ca0b..0060b7b74 100644 --- a/light/rpc/client.go +++ b/light/rpc/client.go @@ -451,6 +451,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. @@ -535,7 +569,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 5bd7b398a..d18671fd0 100644 --- a/rpc/client/http/http.go +++ b/rpc/client/http/http.go @@ -92,9 +92,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 @@ -450,6 +452,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 0fbc55717..e23d2f563 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 9f5ba0072..799639a04 100644 --- a/rpc/client/local/local.go +++ b/rpc/client/local/local.go @@ -165,6 +165,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 index 7012e1c2d..3c3ebd443 100644 --- a/rpc/client/mocks/client.go +++ b/rpc/client/mocks/client.go @@ -457,6 +457,52 @@ 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) { ret := _m.Called(_a0) diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 12c13d686..b2526176a 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -359,6 +359,15 @@ func TestClientMethodCalls(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) @@ -551,7 +560,6 @@ func TestClientMethodCalls(t *testing.T) { _, err := c.BroadcastEvidence(ctx, fake) require.Error(t, err, "BroadcastEvidence(%s) succeeded, but the evidence was fake", fake) } - }) t.Run("BroadcastEmpty", func(t *testing.T) { _, err := c.BroadcastEvidence(ctx, nil) @@ -732,7 +740,6 @@ func TestClientMethodCallsAdvanced(t *testing.T) { for _, c := range GetClients(t, n, conf) { t.Run(fmt.Sprintf("%T", c), func(t *testing.T) { - // now we query for the tx. result, err := c.TxSearch(ctx, fmt.Sprintf("tx.hash='%v'", find.Hash), true, nil, nil, "asc") require.Nil(t, 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: