diff --git a/vms/platformvm/client.go b/vms/platformvm/client.go index e6998be657f..9b1bab2feec 100644 --- a/vms/platformvm/client.go +++ b/vms/platformvm/client.go @@ -261,6 +261,8 @@ type Client interface { ) (map[ids.NodeID]*validators.GetValidatorOutput, error) // GetBlock returns the block with the given id. GetBlock(ctx context.Context, blockID ids.ID, options ...rpc.Option) ([]byte, error) + // GetBlockByHeight returns the block at the given [height]. + GetBlockByHeight(ctx context.Context, height uint64, options ...rpc.Option) ([]byte, error) } // Client implementation for interacting with the P Chain endpoint @@ -877,3 +879,15 @@ func (c *client) GetBlock(ctx context.Context, blockID ids.ID, options ...rpc.Op } return formatting.Decode(res.Encoding, res.Block) } + +func (c *client) GetBlockByHeight(ctx context.Context, height uint64, options ...rpc.Option) ([]byte, error) { + res := &api.FormattedBlock{} + err := c.requester.SendRequest(ctx, "platform.getBlockByHeight", &api.GetBlockByHeightArgs{ + Height: json.Uint64(height), + Encoding: formatting.HexNC, + }, res, options...) + if err != nil { + return nil, err + } + return formatting.Decode(res.Encoding, res.Block) +} diff --git a/vms/platformvm/service.go b/vms/platformvm/service.go index 76757cefdf3..9e2ea52484c 100644 --- a/vms/platformvm/service.go +++ b/vms/platformvm/service.go @@ -2651,6 +2651,44 @@ func (s *Service) GetBlock(_ *http.Request, args *api.GetBlockArgs, response *ap return nil } +// GetBlockByHeight returns the block at the given height. +func (s *Service) GetBlockByHeight(_ *http.Request, args *api.GetBlockByHeightArgs, response *api.GetBlockResponse) error { + s.vm.ctx.Log.Debug("API called", + zap.String("service", "platform"), + zap.String("method", "getBlockByHeight"), + zap.Uint64("height", uint64(args.Height)), + zap.Stringer("encoding", args.Encoding), + ) + + blockID, err := s.vm.state.GetBlockIDAtHeight(uint64(args.Height)) + if err != nil { + return fmt.Errorf("couldn't get block at height %d: %w", args.Height, err) + } + + block, err := s.vm.manager.GetStatelessBlock(blockID) + if err != nil { + s.vm.ctx.Log.Error("couldn't get accepted block", + zap.Stringer("blkID", blockID), + zap.Error(err), + ) + return fmt.Errorf("couldn't get block with id %s: %w", blockID, err) + } + response.Encoding = args.Encoding + + if args.Encoding == formatting.JSON { + block.InitCtx(s.vm.ctx) + response.Block = block + return nil + } + + response.Block, err = formatting.Encode(args.Encoding, block.Bytes()) + if err != nil { + return fmt.Errorf("couldn't encode block %s as %s: %w", blockID, args.Encoding, err) + } + + return nil +} + func (s *Service) getAPIUptime(staker *state.Staker) (*json.Float32, error) { // Only report uptimes that we have been actively tracking. if constants.PrimaryNetworkID != staker.SubnetID && !s.vm.TrackedSubnets.Contains(staker.SubnetID) { diff --git a/vms/platformvm/service_test.go b/vms/platformvm/service_test.go index 5181adba13c..dd023a8057e 100644 --- a/vms/platformvm/service_test.go +++ b/vms/platformvm/service_test.go @@ -14,6 +14,8 @@ import ( stdjson "encoding/json" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/api" @@ -24,6 +26,7 @@ import ( "github.com/ava-labs/avalanchego/database/manager" "github.com/ava-labs/avalanchego/database/prefixdb" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils/constants" @@ -816,3 +819,190 @@ func TestGetValidatorsAtReplyMarshalling(t *testing.T) { require.NoError(parsedReply.UnmarshalJSON(replyJSON)) require.Equal(reply, &parsedReply) } + +func TestServiceGetBlockByHeight(t *testing.T) { + ctrl := gomock.NewController(t) + + blockID := ids.GenerateTestID() + blockHeight := uint64(1337) + + type test struct { + name string + serviceAndExpectedBlockFunc func(t *testing.T, ctrl *gomock.Controller) (*Service, interface{}) + encoding formatting.Encoding + expectedErr error + } + + tests := []test{ + { + name: "block height not found", + serviceAndExpectedBlockFunc: func(_ *testing.T, ctrl *gomock.Controller) (*Service, interface{}) { + state := state.NewMockState(ctrl) + state.EXPECT().GetBlockIDAtHeight(blockHeight).Return(ids.Empty, database.ErrNotFound) + + manager := blockexecutor.NewMockManager(ctrl) + return &Service{ + vm: &VM{ + state: state, + manager: manager, + ctx: &snow.Context{ + Log: logging.NoLog{}, + }, + }, + }, nil + }, + encoding: formatting.Hex, + expectedErr: database.ErrNotFound, + }, + { + name: "block not found", + serviceAndExpectedBlockFunc: func(_ *testing.T, ctrl *gomock.Controller) (*Service, interface{}) { + state := state.NewMockState(ctrl) + state.EXPECT().GetBlockIDAtHeight(blockHeight).Return(blockID, nil) + + manager := blockexecutor.NewMockManager(ctrl) + manager.EXPECT().GetStatelessBlock(blockID).Return(nil, database.ErrNotFound) + return &Service{ + vm: &VM{ + state: state, + manager: manager, + ctx: &snow.Context{ + Log: logging.NoLog{}, + }, + }, + }, nil + }, + encoding: formatting.Hex, + expectedErr: database.ErrNotFound, + }, + { + name: "JSON format", + serviceAndExpectedBlockFunc: func(_ *testing.T, ctrl *gomock.Controller) (*Service, interface{}) { + block := blocks.NewMockBlock(ctrl) + block.EXPECT().InitCtx(gomock.Any()) + + state := state.NewMockState(ctrl) + state.EXPECT().GetBlockIDAtHeight(blockHeight).Return(blockID, nil) + + manager := blockexecutor.NewMockManager(ctrl) + manager.EXPECT().GetStatelessBlock(blockID).Return(block, nil) + return &Service{ + vm: &VM{ + state: state, + manager: manager, + ctx: &snow.Context{ + Log: logging.NoLog{}, + }, + }, + }, block + }, + encoding: formatting.JSON, + expectedErr: nil, + }, + { + name: "hex format", + serviceAndExpectedBlockFunc: func(t *testing.T, ctrl *gomock.Controller) (*Service, interface{}) { + block := blocks.NewMockBlock(ctrl) + blockBytes := []byte("hi mom") + block.EXPECT().Bytes().Return(blockBytes) + + state := state.NewMockState(ctrl) + state.EXPECT().GetBlockIDAtHeight(blockHeight).Return(blockID, nil) + + expected, err := formatting.Encode(formatting.Hex, blockBytes) + require.NoError(t, err) + + manager := blockexecutor.NewMockManager(ctrl) + manager.EXPECT().GetStatelessBlock(blockID).Return(block, nil) + return &Service{ + vm: &VM{ + state: state, + manager: manager, + ctx: &snow.Context{ + Log: logging.NoLog{}, + }, + }, + }, expected + }, + encoding: formatting.Hex, + expectedErr: nil, + }, + { + name: "hexc format", + serviceAndExpectedBlockFunc: func(t *testing.T, ctrl *gomock.Controller) (*Service, interface{}) { + block := blocks.NewMockBlock(ctrl) + blockBytes := []byte("hi mom") + block.EXPECT().Bytes().Return(blockBytes) + + state := state.NewMockState(ctrl) + state.EXPECT().GetBlockIDAtHeight(blockHeight).Return(blockID, nil) + + expected, err := formatting.Encode(formatting.HexC, blockBytes) + require.NoError(t, err) + + manager := blockexecutor.NewMockManager(ctrl) + manager.EXPECT().GetStatelessBlock(blockID).Return(block, nil) + return &Service{ + vm: &VM{ + state: state, + manager: manager, + ctx: &snow.Context{ + Log: logging.NoLog{}, + }, + }, + }, expected + }, + encoding: formatting.HexC, + expectedErr: nil, + }, + { + name: "hexnc format", + serviceAndExpectedBlockFunc: func(t *testing.T, ctrl *gomock.Controller) (*Service, interface{}) { + block := blocks.NewMockBlock(ctrl) + blockBytes := []byte("hi mom") + block.EXPECT().Bytes().Return(blockBytes) + + state := state.NewMockState(ctrl) + state.EXPECT().GetBlockIDAtHeight(blockHeight).Return(blockID, nil) + + expected, err := formatting.Encode(formatting.HexNC, blockBytes) + require.NoError(t, err) + + manager := blockexecutor.NewMockManager(ctrl) + manager.EXPECT().GetStatelessBlock(blockID).Return(block, nil) + return &Service{ + vm: &VM{ + state: state, + manager: manager, + ctx: &snow.Context{ + Log: logging.NoLog{}, + }, + }, + }, expected + }, + encoding: formatting.HexNC, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + service, expected := tt.serviceAndExpectedBlockFunc(t, ctrl) + + args := &api.GetBlockByHeightArgs{ + Height: json.Uint64(blockHeight), + Encoding: tt.encoding, + } + reply := &api.GetBlockResponse{} + err := service.GetBlockByHeight(nil, args, reply) + require.ErrorIs(err, tt.expectedErr) + if tt.expectedErr == nil { + return + } + require.Equal(tt.encoding, reply.Encoding) + require.Equal(expected, reply.Block) + }) + } +}