diff --git a/RELEASES.md b/RELEASES.md index caa0bd3f516b..0e44c8de5a2c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -5,6 +5,7 @@ ### APIs - Added `platform.getSubnetOnlyValidator` +- Added `platform.getProposedHeight` ### Configs diff --git a/vms/platformvm/client.go b/vms/platformvm/client.go index f795594f6a00..9507d4694d85 100644 --- a/vms/platformvm/client.go +++ b/vms/platformvm/client.go @@ -29,6 +29,8 @@ var _ Client = (*client)(nil) type Client interface { // GetHeight returns the current block height of the P Chain GetHeight(ctx context.Context, options ...rpc.Option) (uint64, error) + // GetProposedHeight returns the current height of this node's proposer VM. + GetProposedHeight(ctx context.Context, options ...rpc.Option) (uint64, error) // ExportKey returns the private key corresponding to [address] from [user]'s account // // Deprecated: Keys should no longer be stored on the node. @@ -152,6 +154,12 @@ func (c *client) GetHeight(ctx context.Context, options ...rpc.Option) (uint64, return uint64(res.Height), err } +func (c *client) GetProposedHeight(ctx context.Context, options ...rpc.Option) (uint64, error) { + res := &api.GetHeightResponse{} + err := c.requester.SendRequest(ctx, "platform.getProposedHeight", struct{}{}, res, options...) + return uint64(res.Height), err +} + func (c *client) ExportKey(ctx context.Context, user api.UserPass, address ids.ShortID, options ...rpc.Option) (*secp256k1.PrivateKey, error) { res := &ExportKeyReply{} err := c.requester.SendRequest(ctx, "platform.exportKey", &ExportKeyArgs{ diff --git a/vms/platformvm/service.go b/vms/platformvm/service.go index f48421c288a3..e5095926d2ea 100644 --- a/vms/platformvm/service.go +++ b/vms/platformvm/service.go @@ -100,6 +100,21 @@ func (s *Service) GetHeight(r *http.Request, _ *struct{}, response *api.GetHeigh return err } +// GetProposedHeight returns the current ProposerVM height +func (s *Service) GetProposedHeight(r *http.Request, _ *struct{}, reply *api.GetHeightResponse) error { + s.vm.ctx.Log.Debug("API called", + zap.String("service", "platform"), + zap.String("method", "getProposedHeight"), + ) + s.vm.ctx.Lock.Lock() + defer s.vm.ctx.Lock.Unlock() + + ctx := r.Context() + proposerHeight, err := s.vm.GetMinimumHeight(ctx) + reply.Height = avajson.Uint64(proposerHeight) + return err +} + // ExportKeyArgs are arguments for ExportKey type ExportKeyArgs struct { api.UserPass diff --git a/vms/platformvm/service.md b/vms/platformvm/service.md index 0c192e1c7e46..fb34d57ba248 100644 --- a/vms/platformvm/service.md +++ b/vms/platformvm/service.md @@ -900,6 +900,42 @@ curl -X POST --data '{ } ``` +### `platform.getProposedHeight` + +Returns this node's current proposer VM height + +**Signature:** + +```sh +platform.getProposedHeight() -> +{ + height: int, +} +``` + +**Example Call:** + +```sh +curl -X POST --data '{ + "jsonrpc": "2.0", + "method": "platform.getProposedHeight", + "params": {}, + "id": 1 +}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/P +``` + +**Example Response:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "height": "56" + }, + "id": 1 +} +``` + ### `platform.getMaxStakeAmount` :::caution diff --git a/vms/platformvm/service_test.go b/vms/platformvm/service_test.go index 2eeeedf20216..cc95a5c5f93b 100644 --- a/vms/platformvm/service_test.go +++ b/vms/platformvm/service_test.go @@ -10,6 +10,7 @@ import ( "fmt" "math" "math/rand" + "net/http" "testing" "time" @@ -91,6 +92,70 @@ func defaultService(t *testing.T, fork upgradetest.Fork) (*Service, *mutableShar }, mutableSharedMemory } +func TestGetProposedHeight(t *testing.T) { + require := require.New(t) + service, _ := defaultService(t, upgradetest.Latest) + + reply := api.GetHeightResponse{} + require.NoError(service.GetProposedHeight(&http.Request{}, nil, &reply)) + + minHeight, err := service.vm.GetMinimumHeight(context.Background()) + require.NoError(err) + require.Equal(minHeight, uint64(reply.Height)) + + service.vm.ctx.Lock.Lock() + + // issue any transaction to put into the new block + subnetID := testSubnet1.ID() + wallet := newWallet(t, service.vm, walletConfig{ + subnetIDs: []ids.ID{subnetID}, + }) + + tx, err := wallet.IssueCreateChainTx( + subnetID, + []byte{}, + constants.AVMID, + []ids.ID{}, + "chain name", + common.WithMemo([]byte{}), + ) + require.NoError(err) + + service.vm.ctx.Lock.Unlock() + + // Get the last accepted block which should be genesis + genesisBlockID := service.vm.manager.LastAccepted() + + require.NoError(service.vm.Network.IssueTxFromRPC(tx)) + service.vm.ctx.Lock.Lock() + + block, err := service.vm.BuildBlock(context.Background()) + require.NoError(err) + + blk := block.(*blockexecutor.Block) + require.NoError(blk.Verify(context.Background())) + + require.NoError(blk.Accept(context.Background())) + + service.vm.ctx.Lock.Unlock() + + latestBlockID := service.vm.manager.LastAccepted() + latestBlock, err := service.vm.manager.GetBlock(latestBlockID) + require.NoError(err) + require.NotEqual(genesisBlockID, latestBlockID) + + // Confirm that the proposed height hasn't changed with the new block being accepted. + require.NoError(service.GetProposedHeight(&http.Request{}, nil, &reply)) + require.Equal(minHeight, uint64(reply.Height)) + + // Set the clock to beyond the proposer VM height of the most recent accepted block + service.vm.clock.Set(latestBlock.Timestamp().Add(31 * time.Second)) + + // Confirm that the proposed height has updated to the latest block height + require.NoError(service.GetProposedHeight(&http.Request{}, nil, &reply)) + require.Equal(latestBlock.Height(), uint64(reply.Height)) +} + func TestExportKey(t *testing.T) { require := require.New(t)