From e2929b6ec0b7d7adbaf78ddd50c9a18f3132e7ea Mon Sep 17 00:00:00 2001 From: Raneet Debnath <35629432+Raneet10@users.noreply.github.com> Date: Wed, 16 Feb 2022 01:44:15 +0530 Subject: [PATCH] Implement BlockChainInfo RPC method (#282) --- CHANGELOG-PENDING.md | 1 + conv/abci/block.go | 16 +++++++ rpc/client/client.go | 67 +++++++++++++++++++++++++++++- rpc/client/client_test.go | 87 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 2 deletions(-) diff --git a/CHANGELOG-PENDING.md b/CHANGELOG-PENDING.md index 1ab424d7c..c3c59ac0d 100644 --- a/CHANGELOG-PENDING.md +++ b/CHANGELOG-PENDING.md @@ -26,6 +26,7 @@ Month, DD, YYYY - [rpc] [Implement Tx Method #272](https://github.com/celestiaorg/optimint/pull/272) [@mauriceLC92](https://github.com/mauriceLC92) - [rpc] [Implement Commit and BlockSearch method #258](https://github.com/celestiaorg/optimint/pull/258) [@raneet10](https://github.com/Raneet10/) - [rpc] [Remove extra variable #280](https://github.com/celestiaorg/optimint/pull/280) [@raneet10](https://github.com/Raneet10/) +- [rpc] [Implement BlockChainInfo RPC method #282](https://github.com/celestiaorg/optimint/pull/282) [@raneet10](https://github.com/Raneet10/) ### BUG FIXES diff --git a/conv/abci/block.go b/conv/abci/block.go index 82be371e4..312b755a2 100644 --- a/conv/abci/block.go +++ b/conv/abci/block.go @@ -98,6 +98,22 @@ func ToABCIBlock(block *types.Block) (*tmtypes.Block, error) { return &abciBlock, nil } +// ToABCIBlockMeta converts Optimint block into BlockMeta format defined by ABCI +func ToABCIBlockMeta(block *types.Block) (*tmtypes.BlockMeta, error) { + tmblock, err := ToABCIBlock(block) + if err != nil { + return nil, err + } + blockId := tmtypes.BlockID{Hash: tmblock.Hash()} + + return &tmtypes.BlockMeta{ + BlockID: blockId, + BlockSize: tmblock.Size(), + Header: tmblock.Header, + NumTxs: len(tmblock.Txs), + }, nil +} + // ToABCICommit converts Optimint commit into commit format defined by ABCI. // This function only converts fields that are available in Optimint commit. // Other fields (especially ValidatorAddress and Timestamp of Signature) has to be filled by caller. diff --git a/rpc/client/client.go b/rpc/client/client.go index 0f195905b..87de07b5c 100644 --- a/rpc/client/client.go +++ b/rpc/client/client.go @@ -267,8 +267,40 @@ func (c *Client) GenesisChunked(context context.Context, id uint) (*ctypes.Resul } func (c *Client) BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { - // needs block store - panic("BlockchainInfo - not implemented!") + const limit int64 = 20 + + // Currently blocks are not pruned and are synced linearly so the base height is 0 + minHeight, maxHeight, err := filterMinMax( + 0, + int64(c.node.Store.Height()), + minHeight, + maxHeight, + limit) + if err != nil { + return nil, err + } + c.Logger.Debug("BlockchainInfo", "maxHeight", maxHeight, "minHeight", minHeight) + + blocks := make([]*types.BlockMeta, 0, maxHeight-minHeight+1) + for height := maxHeight; height >= minHeight; height-- { + block, err := c.node.Store.LoadBlock(uint64(height)) + if err != nil { + return nil, err + } + if block != nil { + tmblockmeta, err := abciconv.ToABCIBlockMeta(block) + if err != nil { + return nil, err + } + blocks = append(blocks, tmblockmeta) + } + } + + return &ctypes.ResultBlockchainInfo{ + LastHeight: int64(c.node.Store.Height()), + BlockMetas: blocks, + }, nil + } func (c *Client) NetInfo(ctx context.Context) (*ctypes.ResultNetInfo, error) { @@ -749,3 +781,34 @@ func validateSkipCount(page, perPage int) int { return skipCount } + +func filterMinMax(base, height, min, max, limit int64) (int64, int64, error) { + // filter negatives + if min < 0 || max < 0 { + return min, max, errors.New("height must be greater than zero") + } + + // adjust for default values + if min == 0 { + min = 1 + } + if max == 0 { + max = height + } + + // limit max to the height + max = tmmath.MinInt64(height, max) + + // limit min to the base + min = tmmath.MaxInt64(base, min) + + // limit min to within `limit` of max + // so the total number of blocks returned will be `limit` + min = tmmath.MaxInt64(min, max-limit+1) + + if min > max { + return min, max, fmt.Errorf("%w: min height %d can't be greater than max height %d", + errors.New("invalid request"), min, max) + } + return min, max, nil +} diff --git a/rpc/client/client_test.go b/rpc/client/client_test.go index 0ae6a983f..4465d6592 100644 --- a/rpc/client/client_test.go +++ b/rpc/client/client_test.go @@ -22,6 +22,7 @@ import ( tmtypes "github.com/tendermint/tendermint/types" "github.com/celestiaorg/optimint/config" + abciconv "github.com/celestiaorg/optimint/conv/abci" "github.com/celestiaorg/optimint/mocks" "github.com/celestiaorg/optimint/node" "github.com/celestiaorg/optimint/state" @@ -498,6 +499,79 @@ func TestConsensusState(t *testing.T) { assert.ErrorIs(err, ErrConsensusStateNotAvailable) } +func TestBlockchainInfo(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + mockApp, rpc := getRPC(t) + mockApp.On("BeginBlock", mock.Anything).Return(abci.ResponseBeginBlock{}) + mockApp.On("Commit", mock.Anything).Return(abci.ResponseCommit{}) + + heights := []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + for _, h := range heights { + block := getRandomBlock(uint64(h), 5) + err := rpc.node.Store.SaveBlock(block, &types.Commit{ + Height: uint64(h), + HeaderHash: block.Header.Hash(), + }) + require.NoError(err) + } + + tests := []struct { + desc string + min int64 + max int64 + exp []*tmtypes.BlockMeta + err bool + }{ + { + desc: "min = 1 and max = 5", + min: 1, + max: 5, + exp: []*tmtypes.BlockMeta{getBlockMeta(rpc, 1), getBlockMeta(rpc, 5)}, + err: false, + }, { + desc: "min height is 0", + min: 0, + max: 10, + exp: []*tmtypes.BlockMeta{getBlockMeta(rpc, 1), getBlockMeta(rpc, 10)}, + err: false, + }, { + desc: "max height is out of range", + min: 0, + max: 15, + exp: []*tmtypes.BlockMeta{getBlockMeta(rpc, 1), getBlockMeta(rpc, 10)}, + err: false, + }, { + desc: "negative min height", + min: -1, + max: 11, + exp: nil, + err: true, + }, { + desc: "negative max height", + min: 1, + max: -1, + exp: nil, + err: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + result, err := rpc.BlockchainInfo(context.Background(), test.min, test.max) + if test.err { + require.Error(err) + } else { + require.NoError(err) + assert.Equal(result.LastHeight, heights[9]) + assert.Contains(result.BlockMetas, test.exp[0]) + assert.Contains(result.BlockMetas, test.exp[1]) + } + + }) + } +} + // copy-pasted from store/store_test.go func getRandomBlock(height uint64, nTxs int) *types.Block { block := &types.Block{ @@ -546,6 +620,19 @@ func getRandomBytes(n int) []byte { return data } +func getBlockMeta(rpc *Client, n int64) *tmtypes.BlockMeta { + b, err := rpc.node.Store.LoadBlock(uint64(n)) + if err != nil { + return nil + } + bmeta, err := abciconv.ToABCIBlockMeta(b) + if err != nil { + return nil + } + + return bmeta +} + func getRPC(t *testing.T) (*mocks.Application, *Client) { t.Helper() require := require.New(t)