Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions vms/platformvm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,21 @@ func (c *Client) GetTimestamp(ctx context.Context, options ...rpc.Option) (time.
return res.Timestamp, err
}

// GetAllValidatorsAt returns the canonical validator sets of
// all chains with at least one active validator at the specified
// height or at proposerVM height if set to [platformapi.ProposedHeight].
func (c *Client) GetAllValidatorsAt(
ctx context.Context,
height platformapi.Height,
options ...rpc.Option,
) (map[ids.ID]validators.WarpSet, error) {
res := &GetAllValidatorsAtReply{}
err := c.Requester.SendRequest(ctx, "platform.getAllValidatorsAt", &GetAllValidatorsAtArgs{
Height: height,
}, res, options...)
return res.ValidatorSets, err
}

// GetValidatorsAt returns the weights of the validator set of a provided subnet
// at the specified height or at proposerVM height if set to
// [platformapi.ProposedHeight].
Expand Down
145 changes: 145 additions & 0 deletions vms/platformvm/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1782,6 +1782,151 @@ func (s *Service) GetTimestamp(_ *http.Request, _ *struct{}, reply *GetTimestamp
return nil
}

// GetAllValidatorsAtArgs are the arguments for GetAllValidatorsAt
type GetAllValidatorsAtArgs struct {
Height platformapi.Height `json:"height"`
}

// GetAllValidatorsAtReply is the response from GetAllValidatorsAt
type GetAllValidatorsAtReply struct {
ValidatorSets map[ids.ID]validators.WarpSet `json:"validatorSets"`
}

type jsonWarpSet struct {
Validators []*jsonWarpValidatorOutput `json:"validators"`
TotalWeight avajson.Uint64 `json:"totalWeight"`
}

type jsonWarpValidatorOutput struct {
PublicKey *string `json:"publicKey"`
Weight avajson.Uint64 `json:"weight"`
NodeIDs []ids.NodeID `json:"nodeIDs"`
}

// GetValidatorsAt returns the weights of the validator set of a provided subnet
// at the specified height.
func (s *Service) GetAllValidatorsAt(r *http.Request, args *GetAllValidatorsAtArgs, reply *GetAllValidatorsAtReply) error {
s.vm.ctx.Log.Debug("API called",
zap.String("service", "platform"),
zap.String("method", "getValidatorsAt"),
zap.Uint64("height", uint64(args.Height)),
zap.Bool("isProposed", args.Height.IsProposed()),
)

s.vm.ctx.Lock.Lock()
defer s.vm.ctx.Lock.Unlock()

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.ValidatorSets, err = s.vm.GetWarpValidatorSets(ctx, height)
if err != nil {
return fmt.Errorf("failed to get validator set: %w", err)
}
return nil
}

func (v *GetAllValidatorsAtReply) MarshalJSON() ([]byte, error) {
m := make(map[ids.ID]*jsonWarpSet, len(v.ValidatorSets))
for subnetID, vdrs := range v.ValidatorSets {
jsonWarpSet := &jsonWarpSet{
TotalWeight: avajson.Uint64(vdrs.TotalWeight),
Validators: make([]*jsonWarpValidatorOutput, len(vdrs.Validators)),
}

for i, vdr := range vdrs.Validators {
vdrJ, err := warpToJsonWarpValidatorOutput(vdr)
if err != nil {
return nil, err
}

jsonWarpSet.Validators[i] = vdrJ
}

m[subnetID] = jsonWarpSet
}
return json.Marshal(m)
}

func warpToJsonWarpValidatorOutput(vdr *validators.Warp) (*jsonWarpValidatorOutput, error) {
vdrJSON := &jsonWarpValidatorOutput{
Weight: avajson.Uint64(vdr.Weight),
NodeIDs: vdr.NodeIDs,
}

if vdr.PublicKey != nil {
pk, err := formatting.Encode(formatting.HexNC, bls.PublicKeyToCompressedBytes(vdr.PublicKey))
if err != nil {
return nil, err
}
vdrJSON.PublicKey = &pk
}

return vdrJSON, nil
}

func (v *GetAllValidatorsAtReply) UnmarshalJSON(b []byte) error {
var m map[ids.ID]*jsonWarpSet
if err := json.Unmarshal(b, &m); err != nil {
return err
}

if m == nil {
v.ValidatorSets = nil
return nil
}

v.ValidatorSets = make(map[ids.ID]validators.WarpSet, len(m))
for subnetID, vdrJSON := range m {
warpSet := validators.WarpSet{
TotalWeight: uint64(vdrJSON.TotalWeight),
Validators: make([]*validators.Warp, len(vdrJSON.Validators)),
}

for i, vdrJSON := range vdrJSON.Validators {
vdr, err := jsonWarpValidatorOutputToWarp(vdrJSON)
if err != nil {
return err
}

warpSet.Validators[i] = vdr
}

v.ValidatorSets[subnetID] = warpSet
}
return nil
}

func jsonWarpValidatorOutputToWarp(vdrJSON *jsonWarpValidatorOutput) (*validators.Warp, error) {
vdr := &validators.Warp{
Weight: uint64(vdrJSON.Weight),
NodeIDs: vdrJSON.NodeIDs,
}

if vdrJSON.PublicKey != nil {
pkBytes, err := formatting.Decode(formatting.HexNC, *vdrJSON.PublicKey)
if err != nil {
return nil, err
}

vdr.PublicKey, err = bls.PublicKeyFromCompressedBytes(pkBytes)
if err != nil {
return nil, err
}

vdr.PublicKeyBytes = vdr.PublicKey.Serialize()
}

return vdr, nil
}

// GetValidatorsAtArgs is the response from GetValidatorsAt
type GetValidatorsAtArgs struct {
Height platformapi.Height `json:"height"`
Expand Down
73 changes: 73 additions & 0 deletions vms/platformvm/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -1830,6 +1830,79 @@ curl -X POST --data '{

**Example Response:**

```json
{
"jsonrpc": "2.0",
"result": {
"2JcZwv2xXxiFHSpRjBaGMK93D61zdyKx2piP95K27ykyUgqhAY": {
"validators": [
{
"publicKey": "0x97c71318cde9fe6839c30e1832c70983dbe7c9b0b371b0f582f9889612bf08039e51025598b41fa46b45e2a3376f03f4",
"weight": "200",
"nodeIDs": [
"NodeID-ADfrGxnezauCF7kUrEoyLzbx5UFaJQc53"
]
}
],
"totalWeight": "200"
},
"u3Jjpzzj95827jdENvR1uc76f4zvvVQjGshbVWaSr2Ce5WV1H": {
"validators": [
{
"publicKey": "0xab0d56c98593744c5604a8ee4713ee139bf583eb2bc66bfaad66376f5d351ee657627cff184dfb27c278d9d6da9930d6",
"weight": "1000",
"nodeIDs": [
"NodeID-JEDBLtsdi2S8JvCjfStpcSLLaRmSPuApv"
]
},
{
"publicKey": "0x96935382d34035816802ab6fc4eb29e60e2cf3164e8e9d3419339f3f09c8cd09ffe8c83c21c02f225a4b9e810453f729",
"weight": "500",
"nodeIDs": [
"NodeID-NmcC3gCqnCHUpWxLSmtvN9oCcBycZMfqM",
"NodeID-2XcmyLqKMPuCCZqfrWuqNQREKrwMwv4e8"
]
}
],
"totalWeight": "1500"
}
},
"id": 1
}
```

### `platform.getAllValidatorsAt`

Get the validators and their weights of all Subnets and the Primary Network at a given P-Chain height.

**Signature:**

```
platform.getAllValidatorsAt(
{
height: [int|string],
}
)
```

- `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.

**Example Call:**

```bash
curl -X POST --data '{
"jsonrpc": "2.0",
"method": "platform.getAllValidatorsAt",
"params": {
"height":1
},
"id": 1
}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/P
```

**Example Response:**

```json
{
"jsonrpc": "2.0",
Expand Down
77 changes: 77 additions & 0 deletions vms/platformvm/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,83 @@ func TestGetValidatorsAt(t *testing.T) {
require.Len(response.Validators, len(genesis.Validators)+1)
}

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

// Create test subnet IDs
subnetID1 := ids.GenerateTestID()
subnetID2 := ids.GenerateTestID()

// Create test node IDs
nodeID1 := ids.GenerateTestNodeID()
nodeID2 := ids.GenerateTestNodeID()
nodeID3 := ids.GenerateTestNodeID()
nodeID4 := ids.GenerateTestNodeID()

// Create test BLS keys
sk1, err := localsigner.New()
require.NoError(err)
sk2, err := localsigner.New()
require.NoError(err)
sk3, err := localsigner.New()
require.NoError(err)

reply := &GetAllValidatorsAtReply{
ValidatorSets: make(map[ids.ID]validators.WarpSet),
}

// Add first subnet with validators having public keys
reply.ValidatorSets[subnetID1] = validators.WarpSet{
TotalWeight: 1500,
Validators: []*validators.Warp{
{
PublicKey: sk1.PublicKey(),
PublicKeyBytes: bls.PublicKeyToUncompressedBytes(sk1.PublicKey()),
Weight: 1000,
NodeIDs: []ids.NodeID{nodeID1},
},
{
PublicKey: sk2.PublicKey(),
PublicKeyBytes: bls.PublicKeyToUncompressedBytes(sk2.PublicKey()),
Weight: 500,
NodeIDs: []ids.NodeID{nodeID2, nodeID3},
},
},
}

// Add second subnet with no validators (empty set)
reply.ValidatorSets[subnetID2] = validators.WarpSet{
TotalWeight: 200,
Validators: []*validators.Warp{
{
PublicKey: sk3.PublicKey(),
PublicKeyBytes: bls.PublicKeyToUncompressedBytes(sk3.PublicKey()),
Weight: 200,
NodeIDs: []ids.NodeID{nodeID4},
},
},
}

// Test marshalling
replyJSON, err := reply.MarshalJSON()
require.NoError(err)

// Test unmarshalling
var parsedReply GetAllValidatorsAtReply
require.NoError(parsedReply.UnmarshalJSON(replyJSON))

require.Equal(reply, &parsedReply)

// Test that the unmarshalled data has the expected structure
require.Len(parsedReply.ValidatorSets, 2)
require.Contains(parsedReply.ValidatorSets, subnetID1)
require.Contains(parsedReply.ValidatorSets, subnetID2)

// Verify subnet data
require.Equal(reply.ValidatorSets[subnetID1], parsedReply.ValidatorSets[subnetID1])
require.Equal(reply.ValidatorSets[subnetID2], parsedReply.ValidatorSets[subnetID2])
}

func TestGetValidatorsAtArgsMarshalling(t *testing.T) {
subnetID, err := ids.FromString("u3Jjpzzj95827jdENvR1uc76f4zvvVQjGshbVWaSr2Ce5WV1H")
require.NoError(t, err)
Expand Down
Loading