diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 46c8f9c2fa10..e0503c88bf17 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -155,6 +155,21 @@ var _ = e2e.DescribePChain("[L1]", func() { subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, platformapi.Height(height)) require.NoError(err) require.Equal(expectedValidators, subnetValidators) + + // Test GetAllValidatorsAt too, for coverage + flattenedExpectedValidators, err := snowvalidators.FlattenValidatorSet(expectedValidators) // for coverage + require.NoError(err) + + allValidators, err := pClient.GetAllValidatorsAt(tc.DefaultContext(), platformapi.Height(height)) + require.NoError(err) + + if len(expectedValidators) > 0 { + require.Contains(allValidators, subnetID) + require.Equal(allValidators[subnetID].Validators, flattenedExpectedValidators.Validators) + require.Equal(allValidators[subnetID].TotalWeight, flattenedExpectedValidators.TotalWeight) + } else { + require.NotContains(allValidators, subnetID) + } } tc.By("verifying the Permissioned Subnet is configured as expected", func() { tc.By("verifying the subnet reports as permissioned", func() { diff --git a/vms/platformvm/client.go b/vms/platformvm/client.go index 761e3a5d8d7c..c76fac62dfd9 100644 --- a/vms/platformvm/client.go +++ b/vms/platformvm/client.go @@ -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]. diff --git a/vms/platformvm/service.go b/vms/platformvm/service.go index 44f51e701eb5..3d07fa9c456f 100644 --- a/vms/platformvm/service.go +++ b/vms/platformvm/service.go @@ -1782,6 +1782,158 @@ 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 []*jsonWarpValidator `json:"validators"` + TotalWeight avajson.Uint64 `json:"totalWeight"` +} + +type jsonWarpValidator struct { + PublicKey *string `json:"publicKey"` + Weight avajson.Uint64 `json:"weight"` + NodeIDs []ids.NodeID `json:"nodeIDs"` +} + +// 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 (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", "getAllValidatorsAt"), + 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() + height, err := s.getQueryHeight(ctx, args.Height) + if err != nil { + return fmt.Errorf("failed to get query height: %w", err) + } + + reply.ValidatorSets, err = s.vm.GetWarpValidatorSets(ctx, height) + if err != nil { + return fmt.Errorf("failed to get validator sets: %w", err) + } + return nil +} + +// If args.Height is the sentinel value for proposed height, gets the proposed height and return it, +// else returns the input height. +func (s *Service) getQueryHeight(ctx context.Context, heightArg platformapi.Height) (uint64, error) { + if heightArg.IsProposed() { + return s.vm.GetMinimumHeight(ctx) + } + + return uint64(heightArg), 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([]*jsonWarpValidator, len(vdrs.Validators)), + } + + for i, vdr := range vdrs.Validators { + vdrJ, err := warpToJSONWarpValidator(vdr) + if err != nil { + return nil, err + } + + jsonWarpSet.Validators[i] = vdrJ + } + + m[subnetID] = jsonWarpSet + } + return json.Marshal(m) +} + +func warpToJSONWarpValidator(vdr *validators.Warp) (*jsonWarpValidator, error) { + vdrJSON := &jsonWarpValidator{ + 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 *jsonWarpValidator) (*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"` @@ -1867,13 +2019,9 @@ func (s *Service) GetValidatorsAt(r *http.Request, args *GetValidatorsAtArgs, re 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) - } + height, err := s.getQueryHeight(ctx, args.Height) + if err != nil { + return fmt.Errorf("failed to get query height: %w", err) } reply.Validators, err = s.vm.GetValidatorSet(ctx, height, args.SubnetID) diff --git a/vms/platformvm/service.md b/vms/platformvm/service.md index 05e0ba461f2a..6eb7b076b45a 100644 --- a/vms/platformvm/service.md +++ b/vms/platformvm/service.md @@ -1846,6 +1846,79 @@ curl -X POST --data '{ } ``` +### `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], + } +) +``` + +```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", + "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 +} +``` + +- `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:** + ### `platform.getValidatorFeeConfig` Returns the validator fee configuration of the P-Chain. diff --git a/vms/platformvm/service_test.go b/vms/platformvm/service_test.go index 210896a229cf..610e471b10ef 100644 --- a/vms/platformvm/service_test.go +++ b/vms/platformvm/service_test.go @@ -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)