diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 93dc33d458..69c36112bd 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -1211,7 +1211,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/light/proxy/routes.go b/light/proxy/routes.go index 2898853d26..d35d1dabee 100644 --- a/light/proxy/routes.go +++ b/light/proxy/routes.go @@ -28,6 +28,8 @@ func RPCRoutes(c *lrpc.Client) map[string]*rpcserver.RPCFunc { "block_by_hash": rpcserver.NewRPCFunc(makeBlockByHashFunc(c), "hash", rpcserver.Cacheable()), "block_results": rpcserver.NewRPCFunc(makeBlockResultsFunc(c), "height", rpcserver.Cacheable("height")), "commit": rpcserver.NewRPCFunc(makeCommitFunc(c), "height", rpcserver.Cacheable("height")), + "header": rpcserver.NewRPCFunc(makeHeaderFunc(c), "height", rpcserver.Cacheable("height")), + "header_by_hash": rpcserver.NewRPCFunc(makeHeaderByHashFunc(c), "hash"), "tx": rpcserver.NewRPCFunc(makeTxFunc(c), "hash,prove", rpcserver.Cacheable()), "tx_search": rpcserver.NewRPCFunc(makeTxSearchFuncMatchEvents(c), "query,prove,page,per_page,order_by,match_events"), "block_search": rpcserver.NewRPCFunc(makeBlockSearchFuncMatchEvents(c), "query,page,per_page,order_by,match_events"), @@ -109,6 +111,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 5fcd31168c..ce3d98afb8 100644 --- a/light/rpc/client.go +++ b/light/rpc/client.go @@ -487,6 +487,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 cmtbytes.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 c16899fe7b..df2b8980e1 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 @@ -457,6 +459,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 e5dfa8ba3c..a6f5be23db 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -68,6 +68,8 @@ type SignClient interface { SignedBlock(ctx context.Context, height *int64) (*ctypes.ResultSignedBlock, 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) DataCommitment(ctx context.Context, start, end uint64) (*ctypes.ResultDataCommitment, error) diff --git a/rpc/client/local/local.go b/rpc/client/local/local.go index 8086b7c3c8..30c578c0fd 100644 --- a/rpc/client/local/local.go +++ b/rpc/client/local/local.go @@ -173,6 +173,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 f8eb7a45ce..a9709d94df 100644 --- a/rpc/client/mocks/client.go +++ b/rpc/client/mocks/client.go @@ -459,6 +459,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 29d6dce133..c5fe5eb40f 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 e7b8cdadcf..09ebe391c2 100644 --- a/rpc/core/blocks.go +++ b/rpc/core/blocks.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/tendermint/tendermint/crypto/merkle" + "github.com/tendermint/tendermint/libs/bytes" cmtmath "github.com/tendermint/tendermint/libs/math" cmtquery "github.com/tendermint/tendermint/libs/pubsub/query" "github.com/tendermint/tendermint/pkg/consts" @@ -87,6 +88,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(GetEnvironment().BlockStore.Height(), heightPtr) + if err != nil { + return nil, err + } + + blockMeta := GetEnvironment().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 := GetEnvironment().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.cometbft.com/v0.34/rpc/#/Info/block diff --git a/rpc/core/blocks_test.go b/rpc/core/blocks_test.go index 31a37f0ef9..00193876a5 100644 --- a/rpc/core/blocks_test.go +++ b/rpc/core/blocks_test.go @@ -13,13 +13,13 @@ import ( "github.com/tendermint/tendermint/crypto/merkle" "github.com/tendermint/tendermint/libs/pubsub/query" cmtrand "github.com/tendermint/tendermint/libs/rand" + "github.com/tendermint/tendermint/types" abci "github.com/tendermint/tendermint/abci/types" cmtstate "github.com/tendermint/tendermint/proto/tendermint/state" 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" ) func TestBlockchainInfo(t *testing.T) { @@ -292,6 +292,7 @@ func (store mockBlockStore) Size() int64 { retur func (mockBlockStore) LoadBaseMeta() *types.BlockMeta { return nil } func (mockBlockStore) LoadBlockByHash(hash []byte) *types.Block { return nil } func (mockBlockStore) LoadBlockPart(height int64, index int) *types.Part { return nil } +func (mockBlockStore) LoadBlockMetaByHash(hash []byte) *types.BlockMeta { 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 } diff --git a/rpc/core/routes.go b/rpc/core/routes.go index 0c5bc0c1a1..b28188a432 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -25,6 +25,8 @@ var Routes = map[string]*rpc.RPCFunc{ "block_by_hash": rpc.NewRPCFunc(BlockByHash, "hash", rpc.Cacheable()), "block_results": rpc.NewRPCFunc(BlockResults, "height", rpc.Cacheable("height")), "commit": rpc.NewRPCFunc(Commit, "height", rpc.Cacheable("height")), + "header": rpc.NewRPCFunc(Header, "height", rpc.Cacheable("height")), + "header_by_hash": rpc.NewRPCFunc(HeaderByHash, "hash"), "data_commitment": rpc.NewRPCFunc(DataCommitment, "start,end"), "check_tx": rpc.NewRPCFunc(CheckTx, "tx"), "tx": rpc.NewRPCFunc(Tx, "hash,prove", rpc.Cacheable()), diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index 08cd821315..04dc85cc8b 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -49,6 +49,11 @@ type ResultSignedBlock struct { ValidatorSet types.ValidatorSet `json:"validator_set"` } +// 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 068d006287..fc493e4d91 100644 --- a/rpc/openapi/openapi.yaml +++ b/rpc/openapi/openapi.yaml @@ -295,7 +295,7 @@ paths: $ref: "#/components/schemas/ErrorResponse" /net_info: get: - summary: Network informations + summary: Network information operationId: net_info tags: - Info @@ -439,6 +439,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 4493a6e3f2..f93f454478 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 2b6c16fed2..6e24af0362 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 3a2fd1aa73..2c6d6ad17e 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 bd117c1afd..5482507770 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -21,7 +21,7 @@ import ( cmtstore "github.com/tendermint/tendermint/proto/tendermint/store" cmtversion "github.com/tendermint/tendermint/proto/tendermint/version" sm "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/state/test/factory" + "github.com/tendermint/tendermint/test/factory" "github.com/tendermint/tendermint/types" cmttime "github.com/tendermint/tendermint/types/time" "github.com/tendermint/tendermint/version" @@ -485,6 +485,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++ { @@ -561,6 +562,26 @@ 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, partSet := state.MakeBlock(state.LastBlockHeight+1, types.Data{Txs: factory.MakeTxs(state.LastBlockHeight+1, 10)}, new(types.Commit), nil, state.Validators.GetProposer().Address) + seenCommit := makeTestCommit(1, cmttime.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()