Skip to content

Commit

Permalink
Add "proposed" optional flag to getValidatorsAt (#3531)
Browse files Browse the repository at this point in the history
Signed-off-by: Ian Suvak <[email protected]>
Co-authored-by: Stephen Buttolph <[email protected]>
  • Loading branch information
iansuvak and StephenButtolph authored Nov 15, 2024
1 parent 480add4 commit ae6b476
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 12 deletions.
1 change: 1 addition & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Added `platform.getL1Validator`
- Added `platform.getProposedHeight`
- Updated `platform.getValidatorsAt` to accept `"proposed"` as valid `height` input

### Configs

Expand Down
3 changes: 2 additions & 1 deletion tests/e2e/p/l1.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
p2ppb "github.com/ava-labs/avalanchego/proto/pb/p2p"
platformvmpb "github.com/ava-labs/avalanchego/proto/pb/platformvm"
snowvalidators "github.com/ava-labs/avalanchego/snow/validators"
platformapi "github.com/ava-labs/avalanchego/vms/platformvm/api"
platformvmvalidators "github.com/ava-labs/avalanchego/vms/platformvm/validators"
warpmessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message"
)
Expand Down Expand Up @@ -156,7 +157,7 @@ var _ = e2e.DescribePChain("[L1]", func() {
height, err := pClient.GetHeight(tc.DefaultContext())
require.NoError(err)

subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height)
subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, platformapi.Height(height))
require.NoError(err)
require.Equal(expectedValidators, subnetValidators)
}
Expand Down
4 changes: 3 additions & 1 deletion tests/e2e/p/validator_sets.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/ava-labs/avalanchego/vms/platformvm"
"github.com/ava-labs/avalanchego/vms/platformvm/txs"
"github.com/ava-labs/avalanchego/vms/secp256k1fx"

platformapi "github.com/ava-labs/avalanchego/vms/platformvm/api"
)

var _ = e2e.DescribePChain("[Validator Sets]", func() {
Expand Down Expand Up @@ -101,7 +103,7 @@ var _ = e2e.DescribePChain("[Validator Sets]", func() {
validatorSet, err := pvmClient.GetValidatorsAt(
tc.DefaultContext(),
constants.PrimaryNetworkID,
height,
platformapi.Height(height),
)
require.NoError(err)
if observedValidatorSet == nil {
Expand Down
49 changes: 49 additions & 0 deletions vms/platformvm/api/height.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package api

import (
"errors"
"math"

avajson "github.com/ava-labs/avalanchego/utils/json"
)

type Height avajson.Uint64

const (
ProposedHeightFlag = "proposed"
ProposedHeight = math.MaxUint64
)

var errInvalidHeight = errors.New("invalid height")

func (h Height) MarshalJSON() ([]byte, error) {
if h == ProposedHeight {
return []byte(`"` + ProposedHeightFlag + `"`), nil
}
return avajson.Uint64(h).MarshalJSON()
}

func (h *Height) UnmarshalJSON(b []byte) error {
if string(b) == ProposedHeightFlag {
*h = ProposedHeight
return nil
}
err := (*avajson.Uint64)(h).UnmarshalJSON(b)
if err != nil {
return errInvalidHeight
}
// MaxUint64 is reserved for proposed height
// return an error if supplied numerically
if uint64(*h) == ProposedHeight {
*h = 0
return errInvalidHeight
}
return nil
}

func (h Height) IsProposed() bool {
return h == ProposedHeight
}
45 changes: 45 additions & 0 deletions vms/platformvm/api/height_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package api

import (
"strconv"
"testing"

"github.com/stretchr/testify/require"
)

func TestMarshallHeight(t *testing.T) {
require := require.New(t)
h := Height(56)
bytes, err := h.MarshalJSON()
require.NoError(err)
require.Equal(`"56"`, string(bytes))

h = Height(ProposedHeight)
bytes, err = h.MarshalJSON()
require.NoError(err)
require.Equal(`"proposed"`, string(bytes))
}

func TestUnmarshallHeight(t *testing.T) {
require := require.New(t)

var h Height

require.NoError(h.UnmarshalJSON([]byte("56")))
require.Equal(Height(56), h)

require.NoError(h.UnmarshalJSON([]byte(ProposedHeightFlag)))
require.Equal(Height(ProposedHeight), h)
require.True(h.IsProposed())

err := h.UnmarshalJSON([]byte("invalid"))
require.ErrorIs(err, errInvalidHeight)
require.Equal(Height(0), h)

err = h.UnmarshalJSON([]byte(`"` + strconv.FormatUint(uint64(ProposedHeight), 10) + `"`))
require.ErrorIs(err, errInvalidHeight)
require.Equal(Height(0), h)
}
11 changes: 7 additions & 4 deletions vms/platformvm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"github.com/ava-labs/avalanchego/vms/platformvm/fx"
"github.com/ava-labs/avalanchego/vms/platformvm/status"
"github.com/ava-labs/avalanchego/vms/secp256k1fx"

platformapi "github.com/ava-labs/avalanchego/vms/platformvm/api"
)

var _ Client = (*client)(nil)
Expand Down Expand Up @@ -119,11 +121,12 @@ type Client interface {
// GetTimestamp returns the current chain timestamp
GetTimestamp(ctx context.Context, options ...rpc.Option) (time.Time, error)
// GetValidatorsAt returns the weights of the validator set of a provided
// subnet at the specified height.
// subnet at the specified height or at proposerVM height if set to
// [platformapi.ProposedHeight]
GetValidatorsAt(
ctx context.Context,
subnetID ids.ID,
height uint64,
height platformapi.Height,
options ...rpc.Option,
) (map[ids.NodeID]*validators.GetValidatorOutput, error)
// GetBlock returns the block with the given id.
Expand Down Expand Up @@ -572,13 +575,13 @@ func (c *client) GetTimestamp(ctx context.Context, options ...rpc.Option) (time.
func (c *client) GetValidatorsAt(
ctx context.Context,
subnetID ids.ID,
height uint64,
height platformapi.Height,
options ...rpc.Option,
) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
res := &GetValidatorsAtReply{}
err := c.requester.SendRequest(ctx, "platform.getValidatorsAt", &GetValidatorsAtArgs{
SubnetID: subnetID,
Height: json.Uint64(height),
Height: height,
}, res, options...)
return res.Validators, err
}
Expand Down
16 changes: 12 additions & 4 deletions vms/platformvm/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1792,8 +1792,8 @@ func (s *Service) GetTimestamp(_ *http.Request, _ *struct{}, reply *GetTimestamp

// GetValidatorsAtArgs is the response from GetValidatorsAt
type GetValidatorsAtArgs struct {
Height avajson.Uint64 `json:"height"`
SubnetID ids.ID `json:"subnetID"`
Height platformapi.Height `json:"height"`
SubnetID ids.ID `json:"subnetID"`
}

type jsonGetValidatorOutput struct {
Expand Down Expand Up @@ -1863,11 +1863,11 @@ type GetValidatorsAtReply struct {
// GetValidatorsAt returns the weights of the validator set of a provided subnet
// at the specified height.
func (s *Service) GetValidatorsAt(r *http.Request, args *GetValidatorsAtArgs, reply *GetValidatorsAtReply) error {
height := uint64(args.Height)
s.vm.ctx.Log.Debug("API called",
zap.String("service", "platform"),
zap.String("method", "getValidatorsAt"),
zap.Uint64("height", height),
zap.Uint64("height", uint64(args.Height)),
zap.Bool("isProposed", args.Height.IsProposed()),
zap.Stringer("subnetID", args.SubnetID),
)

Expand All @@ -1876,6 +1876,14 @@ func (s *Service) GetValidatorsAt(r *http.Request, args *GetValidatorsAtArgs, re

ctx := r.Context()
var err error
height := uint64(args.Height)
if args.Height.IsProposed() {
height, err = s.vm.GetMinimumHeight(ctx)
if err != nil {
return fmt.Errorf("failed to get proposed height: %w", err)
}
}

reply.Validators, err = s.vm.GetValidatorSet(ctx, height, args.SubnetID)
if err != nil {
return fmt.Errorf("failed to get validator set: %w", err)
Expand Down
5 changes: 3 additions & 2 deletions vms/platformvm/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -1891,13 +1891,14 @@ Get the validators and their weights of a Subnet or the Primary Network at a giv
```sh
platform.getValidatorsAt(
{
height: int,
height: [int|string],
subnetID: string, // optional
}
)
```

- `height` is the P-Chain height to get the validator set at.
- `height` is the P-Chain height to get the validator set at, or the string literal "proposed"
to return the validator set at this node's ProposerVM height.
- `subnetID` is the Subnet ID to get the validator set of. If not given, gets validator set of the
Primary Network.

Expand Down
93 changes: 93 additions & 0 deletions vms/platformvm/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,99 @@ func TestGetCurrentValidators(t *testing.T) {
}
}

func TestGetValidatorsAt(t *testing.T) {
require := require.New(t)
service, _ := defaultService(t, upgradetest.Latest)

genesis := genesistest.New(t, genesistest.Config{})

args := GetValidatorsAtArgs{}
response := GetValidatorsAtReply{}

service.vm.ctx.Lock.Lock()
lastAccepted := service.vm.manager.LastAccepted()
lastAcceptedBlk, err := service.vm.manager.GetBlock(lastAccepted)
require.NoError(err)

service.vm.ctx.Lock.Unlock()

// Confirm that it returns the genesis validators given the latest height
args.Height = pchainapi.Height(lastAcceptedBlk.Height())
require.NoError(service.GetValidatorsAt(&http.Request{}, &args, &response))
require.Len(response.Validators, len(genesis.Validators))

service.vm.ctx.Lock.Lock()

wallet := newWallet(t, service.vm, walletConfig{})
rewardsOwner := &secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{ids.GenerateTestShortID()},
}

sk, err := bls.NewSecretKey()
require.NoError(err)

tx, err := wallet.IssueAddPermissionlessValidatorTx(
&txs.SubnetValidator{
Validator: txs.Validator{
NodeID: ids.GenerateTestNodeID(),
Start: uint64(service.vm.clock.Time().Add(txexecutor.SyncBound).Unix()),
End: uint64(service.vm.clock.Time().Add(txexecutor.SyncBound).Add(defaultMinStakingDuration).Unix()),
Wght: service.vm.MinValidatorStake,
},
Subnet: constants.PrimaryNetworkID,
},
signer.NewProofOfPossession(sk),
service.vm.ctx.AVAXAssetID,
rewardsOwner,
rewardsOwner,
0,
common.WithMemo([]byte{}),
)

require.NoError(err)

service.vm.ctx.Lock.Unlock()
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()

newLastAccepted := service.vm.manager.LastAccepted()
newLastAcceptedBlk, err := service.vm.manager.GetBlock(newLastAccepted)
require.NoError(err)
require.NotEqual(newLastAccepted, lastAccepted)

// Confirm that it returns the genesis validators + the new validator given the latest height
args.Height = pchainapi.Height(newLastAcceptedBlk.Height())
require.NoError(service.GetValidatorsAt(&http.Request{}, &args, &response))
require.Len(response.Validators, len(genesis.Validators)+1)

// Confirm that [IsProposed] works. The proposed height should be the genesis height
args.Height = pchainapi.Height(pchainapi.ProposedHeight)
require.NoError(service.GetValidatorsAt(&http.Request{}, &args, &response))
require.Len(response.Validators, len(genesis.Validators))

service.vm.ctx.Lock.Lock()

// set clock beyond the [validators.recentlyAcceptedWindowTTL] to bump the
// proposerVM height
service.vm.clock.Set(newLastAcceptedBlk.Timestamp().Add(40 * time.Second))
service.vm.ctx.Lock.Unlock()

// Resending the same request with [IsProposed] set to true should now
// include the new validator
require.NoError(service.GetValidatorsAt(&http.Request{}, &args, &response))
require.Len(response.Validators, len(genesis.Validators)+1)
}

func TestGetTimestamp(t *testing.T) {
require := require.New(t)
service, _ := defaultService(t, upgradetest.Latest)
Expand Down

0 comments on commit ae6b476

Please sign in to comment.