Skip to content

Commit 7546749

Browse files
Add GetAllValidatorsAt endpoint (#4394)
Co-authored-by: Stephen Buttolph <stephen@avalabs.org>
1 parent 4c432d8 commit 7546749

File tree

6 files changed

+316
-8
lines changed

6 files changed

+316
-8
lines changed

snow/validators/warp.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ package validators
55

66
import (
77
"bytes"
8+
"encoding/json"
89

910
"golang.org/x/exp/maps"
1011

1112
"github.com/ava-labs/avalanchego/ids"
1213
"github.com/ava-labs/avalanchego/utils"
1314
"github.com/ava-labs/avalanchego/utils/crypto/bls"
15+
"github.com/ava-labs/avalanchego/utils/formatting"
1416
"github.com/ava-labs/avalanchego/utils/math"
17+
18+
avajson "github.com/ava-labs/avalanchego/utils/json"
1519
)
1620

1721
var _ utils.Sortable[*Warp] = (*Warp)(nil)
@@ -24,6 +28,28 @@ type WarpSet struct {
2428
TotalWeight uint64
2529
}
2630

31+
type jsonWarpSet struct {
32+
Validators []*Warp `json:"validators"`
33+
TotalWeight avajson.Uint64 `json:"totalWeight"`
34+
}
35+
36+
func (w WarpSet) MarshalJSON() ([]byte, error) {
37+
return json.Marshal(jsonWarpSet{
38+
Validators: w.Validators,
39+
TotalWeight: avajson.Uint64(w.TotalWeight),
40+
})
41+
}
42+
43+
func (w *WarpSet) UnmarshalJSON(b []byte) error {
44+
var j jsonWarpSet
45+
if err := json.Unmarshal(b, &j); err != nil {
46+
return err
47+
}
48+
w.TotalWeight = uint64(j.TotalWeight)
49+
w.Validators = j.Validators
50+
return nil
51+
}
52+
2753
type Warp struct {
2854
PublicKey *bls.PublicKey
2955
// PublicKeyBytes is expected to be in the uncompressed form.
@@ -36,6 +62,48 @@ func (w *Warp) Compare(o *Warp) int {
3662
return bytes.Compare(w.PublicKeyBytes, o.PublicKeyBytes)
3763
}
3864

65+
type jsonWarp struct {
66+
PublicKey string `json:"publicKey"`
67+
Weight avajson.Uint64 `json:"weight"`
68+
NodeIDs []ids.NodeID `json:"nodeIDs"`
69+
}
70+
71+
func (w Warp) MarshalJSON() ([]byte, error) {
72+
pkBytes := bls.PublicKeyToCompressedBytes(w.PublicKey)
73+
pk, err := formatting.Encode(formatting.HexNC, pkBytes)
74+
if err != nil {
75+
return nil, err
76+
}
77+
return json.Marshal(jsonWarp{
78+
PublicKey: pk,
79+
Weight: avajson.Uint64(w.Weight),
80+
NodeIDs: w.NodeIDs,
81+
})
82+
}
83+
84+
func (w *Warp) UnmarshalJSON(b []byte) error {
85+
var j jsonWarp
86+
if err := json.Unmarshal(b, &j); err != nil {
87+
return err
88+
}
89+
90+
pkBytes, err := formatting.Decode(formatting.HexNC, j.PublicKey)
91+
if err != nil {
92+
return err
93+
}
94+
pk, err := bls.PublicKeyFromCompressedBytes(pkBytes)
95+
if err != nil {
96+
return err
97+
}
98+
*w = Warp{
99+
PublicKey: pk,
100+
PublicKeyBytes: bls.PublicKeyToUncompressedBytes(pk),
101+
Weight: uint64(j.Weight),
102+
NodeIDs: j.NodeIDs,
103+
}
104+
return nil
105+
}
106+
39107
// FlattenValidatorSet converts the provided vdrSet into a canonical ordering.
40108
func FlattenValidatorSet(vdrSet map[ids.NodeID]*GetValidatorOutput) (WarpSet, error) {
41109
var (

snow/validators/warp_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package validators_test
55

66
import (
7+
"encoding/hex"
8+
"encoding/json"
79
"math"
810
"strconv"
911
"testing"
@@ -12,13 +14,92 @@ import (
1214

1315
"github.com/ava-labs/avalanchego/ids"
1416
"github.com/ava-labs/avalanchego/snow/validators/validatorstest"
17+
"github.com/ava-labs/avalanchego/utils/crypto/bls"
1518
"github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner"
1619

1720
safemath "github.com/ava-labs/avalanchego/utils/math"
1821

1922
. "github.com/ava-labs/avalanchego/snow/validators"
2023
)
2124

25+
func TestWarpJSON(t *testing.T) {
26+
const pkStr = "88c4760201a451619475ff7d3782d02381826bad5bef306d1ff6b22d3fb2e137bc004d867054efc241463fe4b21c61af"
27+
pkBytes, err := hex.DecodeString(pkStr)
28+
require.NoError(t, err)
29+
pk, err := bls.PublicKeyFromCompressedBytes(pkBytes)
30+
require.NoError(t, err)
31+
32+
w := Warp{
33+
PublicKey: pk,
34+
PublicKeyBytes: bls.PublicKeyToUncompressedBytes(pk),
35+
Weight: 12345,
36+
NodeIDs: []ids.NodeID{
37+
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07},
38+
{0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
39+
},
40+
}
41+
wJSON, err := json.MarshalIndent(w, "", "\t")
42+
require.NoError(t, err)
43+
44+
const expectedJSON = `{
45+
"publicKey": "0x88c4760201a451619475ff7d3782d02381826bad5bef306d1ff6b22d3fb2e137bc004d867054efc241463fe4b21c61af",
46+
"weight": "12345",
47+
"nodeIDs": [
48+
"NodeID-12D2adLM3UJyWgFWD2VXv5vkMT8MoMbg",
49+
"NodeID-jVEbJZmHxdPnxBthD7v8dC96qyUrPMxg"
50+
]
51+
}`
52+
require.JSONEq(t, expectedJSON, string(wJSON))
53+
54+
var parsedW Warp
55+
require.NoError(t, json.Unmarshal(wJSON, &parsedW))
56+
require.Equal(t, w, parsedW)
57+
}
58+
59+
func TestWarpSetJSON(t *testing.T) {
60+
const pkStr = "88c4760201a451619475ff7d3782d02381826bad5bef306d1ff6b22d3fb2e137bc004d867054efc241463fe4b21c61af"
61+
pkBytes, err := hex.DecodeString(pkStr)
62+
require.NoError(t, err)
63+
pk, err := bls.PublicKeyFromCompressedBytes(pkBytes)
64+
require.NoError(t, err)
65+
66+
ws := WarpSet{
67+
Validators: []*Warp{
68+
{
69+
PublicKey: pk,
70+
PublicKeyBytes: bls.PublicKeyToUncompressedBytes(pk),
71+
Weight: 12345,
72+
NodeIDs: []ids.NodeID{
73+
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07},
74+
{0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
75+
},
76+
},
77+
},
78+
TotalWeight: 54321,
79+
}
80+
wsJSON, err := json.MarshalIndent(ws, "", "\t")
81+
require.NoError(t, err)
82+
83+
const expectedJSON = `{
84+
"validators": [
85+
{
86+
"publicKey": "0x88c4760201a451619475ff7d3782d02381826bad5bef306d1ff6b22d3fb2e137bc004d867054efc241463fe4b21c61af",
87+
"weight": "12345",
88+
"nodeIDs": [
89+
"NodeID-12D2adLM3UJyWgFWD2VXv5vkMT8MoMbg",
90+
"NodeID-jVEbJZmHxdPnxBthD7v8dC96qyUrPMxg"
91+
]
92+
}
93+
],
94+
"totalWeight": "54321"
95+
}`
96+
require.JSONEq(t, expectedJSON, string(wsJSON))
97+
98+
var parsedWS WarpSet
99+
require.NoError(t, json.Unmarshal(wsJSON, &parsedWS))
100+
require.Equal(t, ws, parsedWS)
101+
}
102+
22103
func TestFlattenValidatorSet(t *testing.T) {
23104
var (
24105
vdrs = validatorstest.NewWarpSet(t, 3)

tests/e2e/p/l1.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,20 @@ var _ = e2e.DescribePChain("[L1]", func() {
155155
subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, platformapi.Height(height))
156156
require.NoError(err)
157157
require.Equal(expectedValidators, subnetValidators)
158+
159+
// Test GetAllValidatorsAt too, for coverage
160+
flattenedExpectedValidators, err := snowvalidators.FlattenValidatorSet(expectedValidators) // for coverage
161+
require.NoError(err)
162+
163+
// require.Equal will complain if one has a nil slice and the other
164+
// has an empty slice. This avoids that issue.
165+
if len(flattenedExpectedValidators.Validators) == 0 {
166+
flattenedExpectedValidators.Validators = nil
167+
}
168+
169+
allValidators, err := pClient.GetAllValidatorsAt(tc.DefaultContext(), platformapi.Height(height))
170+
require.NoError(err)
171+
require.Equal(flattenedExpectedValidators, allValidators[subnetID])
158172
}
159173
tc.By("verifying the Permissioned Subnet is configured as expected", func() {
160174
tc.By("verifying the subnet reports as permissioned", func() {

vms/platformvm/client.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,21 @@ func (c *Client) GetTimestamp(ctx context.Context, options ...rpc.Option) (time.
491491
return res.Timestamp, err
492492
}
493493

494+
// GetAllValidatorsAt returns the canonical validator sets of
495+
// all chains with at least one active validator at the specified
496+
// height or at proposerVM height if set to [platformapi.ProposedHeight].
497+
func (c *Client) GetAllValidatorsAt(
498+
ctx context.Context,
499+
height platformapi.Height,
500+
options ...rpc.Option,
501+
) (map[ids.ID]validators.WarpSet, error) {
502+
res := &GetAllValidatorsAtReply{}
503+
err := c.Requester.SendRequest(ctx, "platform.getAllValidatorsAt", &GetAllValidatorsAtArgs{
504+
Height: height,
505+
}, res, options...)
506+
return res.ValidatorSets, err
507+
}
508+
494509
// GetValidatorsAt returns the weights of the validator set of a provided subnet
495510
// at the specified height or at proposerVM height if set to
496511
// [platformapi.ProposedHeight].

vms/platformvm/service.go

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1782,6 +1782,57 @@ func (s *Service) GetTimestamp(_ *http.Request, _ *struct{}, reply *GetTimestamp
17821782
return nil
17831783
}
17841784

1785+
// GetAllValidatorsAtArgs are the arguments for GetAllValidatorsAt
1786+
type GetAllValidatorsAtArgs struct {
1787+
Height platformapi.Height `json:"height"`
1788+
}
1789+
1790+
// GetAllValidatorsAtReply is the response from GetAllValidatorsAt
1791+
type GetAllValidatorsAtReply struct {
1792+
ValidatorSets map[ids.ID]validators.WarpSet `json:"validatorSets"`
1793+
}
1794+
1795+
// GetAllValidatorsAt returns the canonical validator sets of
1796+
// all chains with at least one active validator at the specified
1797+
// height or at proposerVM height if set to [platformapi.ProposedHeight].
1798+
func (s *Service) GetAllValidatorsAt(r *http.Request, args *GetAllValidatorsAtArgs, reply *GetAllValidatorsAtReply) error {
1799+
s.vm.ctx.Log.Debug("API called",
1800+
zap.String("service", "platform"),
1801+
zap.String("method", "getAllValidatorsAt"),
1802+
zap.Uint64("height", uint64(args.Height)),
1803+
zap.Bool("isProposed", args.Height.IsProposed()),
1804+
)
1805+
1806+
s.vm.ctx.Lock.Lock()
1807+
defer s.vm.ctx.Lock.Unlock()
1808+
1809+
ctx := r.Context()
1810+
height, err := s.getQueryHeight(ctx, args.Height)
1811+
if err != nil {
1812+
return err
1813+
}
1814+
1815+
reply.ValidatorSets, err = s.vm.GetWarpValidatorSets(ctx, height)
1816+
if err != nil {
1817+
return fmt.Errorf("failed to get validator sets at %d: %w", height, err)
1818+
}
1819+
return nil
1820+
}
1821+
1822+
// If args.Height is the sentinel value for proposed height, gets the proposed height and return it,
1823+
// else returns the input height.
1824+
func (s *Service) getQueryHeight(ctx context.Context, heightArg platformapi.Height) (uint64, error) {
1825+
if !heightArg.IsProposed() {
1826+
return uint64(heightArg), nil
1827+
}
1828+
1829+
height, err := s.vm.GetMinimumHeight(ctx)
1830+
if err != nil {
1831+
return 0, fmt.Errorf("failed to get proposed height: %w", err)
1832+
}
1833+
return height, nil
1834+
}
1835+
17851836
// GetValidatorsAtArgs is the response from GetValidatorsAt
17861837
type GetValidatorsAtArgs struct {
17871838
Height platformapi.Height `json:"height"`
@@ -1867,18 +1918,14 @@ func (s *Service) GetValidatorsAt(r *http.Request, args *GetValidatorsAtArgs, re
18671918
defer s.vm.ctx.Lock.Unlock()
18681919

18691920
ctx := r.Context()
1870-
var err error
1871-
height := uint64(args.Height)
1872-
if args.Height.IsProposed() {
1873-
height, err = s.vm.GetMinimumHeight(ctx)
1874-
if err != nil {
1875-
return fmt.Errorf("failed to get proposed height: %w", err)
1876-
}
1921+
height, err := s.getQueryHeight(ctx, args.Height)
1922+
if err != nil {
1923+
return err
18771924
}
18781925

18791926
reply.Validators, err = s.vm.GetValidatorSet(ctx, height, args.SubnetID)
18801927
if err != nil {
1881-
return fmt.Errorf("failed to get validator set: %w", err)
1928+
return fmt.Errorf("failed to get validator set at %d: %w", height, err)
18821929
}
18831930
return nil
18841931
}

0 commit comments

Comments
 (0)