diff --git a/cli/query/query.go b/cli/query/query.go index 1eb141e3e..2bd29dcc9 100644 --- a/cli/query/query.go +++ b/cli/query/query.go @@ -167,7 +167,7 @@ func queryCandidates(ctx *cli.Context) error { return cli.NewExitError(err, 1) } - vals, err := c.GetNextBlockValidators() + vals, err := c.GetCandidates() if err != nil { return cli.NewExitError(err, 1) } diff --git a/docs/rpc.md b/docs/rpc.md index acf02bdf0..e4c3a0876 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -44,6 +44,7 @@ which would yield the response: | `getblockhash` | | `getblockheader` | | `getblockheadercount` | +| `getcandidates` | | `getcommittee` | | `getconnectioncount` | | `getcontractstate` | diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 835d9c5b3..d08c7d03b 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -557,7 +557,20 @@ func (c *Client) GetUnclaimedGas(address string) (result.UnclaimedGas, error) { return resp, nil } -// GetNextBlockValidators returns the current NEO consensus nodes information and voting status. +// GetCandidates returns the current list of NEO candidate node with voting data and +// validator status. +func (c *Client) GetCandidates() ([]result.Candidate, error) { + var ( + params = request.NewRawParams() + resp = new([]result.Candidate) + ) + if err := c.performRequest("getcandidates", params, resp); err != nil { + return nil, err + } + return *resp, nil +} + +// GetNextBlockValidators returns the current NEO consensus nodes information and voting data. func (c *Client) GetNextBlockValidators() ([]result.Validator, error) { var ( params = request.NewRawParams() diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 8b2d92854..b0876a479 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -940,6 +940,21 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ }, }, }, + "getcandidates": { + { + name: "positive", + invoke: func(c *Client) (interface{}, error) { + return c.GetCandidates() + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":[{"publickey":"02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2","votes":"0","active":true},{"publickey":"02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e","votes":"0","active":true},{"publickey":"03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699","votes":"0","active":true},{"publickey":"02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62","votes":"0","active":true}]}`, + result: func(c *Client) interface{} { return []result.Candidate{} }, + check: func(t *testing.T, c *Client, uns interface{}) { + res, ok := uns.([]result.Candidate) + require.True(t, ok) + assert.Equal(t, 4, len(res)) + }, + }, + }, "getvalidators": { { name: "positive", @@ -1657,6 +1672,12 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{ return c.GetUnclaimedGas("") }, }, + { + name: "getcandidates_unmarshalling_error", + invoke: func(c *Client) (interface{}, error) { + return c.GetCandidates() + }, + }, { name: "getvalidators_unmarshalling_error", invoke: func(c *Client) (interface{}, error) { diff --git a/pkg/rpc/response/result/validator.go b/pkg/rpc/response/result/validator.go index 37862511d..f5aafd5b5 100644 --- a/pkg/rpc/response/result/validator.go +++ b/pkg/rpc/response/result/validator.go @@ -1,13 +1,51 @@ package result import ( + "encoding/json" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" ) -// Validator used for the representation of -// state.Validator on the RPC Server. +// Validator is used for the representation of consensus node data in the JSON-RPC +// protocol. type Validator struct { + PublicKey keys.PublicKey `json:"publickey"` + Votes int64 `json:"votes"` +} + +// Candidate represents a node participating in the governance elections, it's +// active when it's a validator (consensus node). +type Candidate struct { PublicKey keys.PublicKey `json:"publickey"` Votes int64 `json:"votes,string"` Active bool `json:"active"` } + +type newValidator struct { + PublicKey keys.PublicKey `json:"publickey"` + Votes int64 `json:"votes"` +} + +type oldValidator struct { + PublicKey keys.PublicKey `json:"publickey"` + Votes int64 `json:"votes,string"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (v *Validator) UnmarshalJSON(data []byte) error { + var nv = new(newValidator) + err := json.Unmarshal(data, nv) + if err != nil { + var ov = new(oldValidator) + err := json.Unmarshal(data, ov) + if err != nil { + return err + } + v.PublicKey = ov.PublicKey + v.Votes = ov.Votes + return nil + } + v.PublicKey = nv.PublicKey + v.Votes = nv.Votes + return nil +} diff --git a/pkg/rpc/response/result/validator_test.go b/pkg/rpc/response/result/validator_test.go new file mode 100644 index 000000000..f7ecd4b68 --- /dev/null +++ b/pkg/rpc/response/result/validator_test.go @@ -0,0 +1,22 @@ +package result + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidatorUnmarshal(t *testing.T) { + old := []byte(`{"publickey":"02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62","votes":"100500","active":true}`) + v := new(Validator) + require.NoError(t, json.Unmarshal(old, v)) + require.Equal(t, int64(100500), v.Votes) + + new := []byte(`{"publickey":"02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62","votes":42}`) + require.NoError(t, json.Unmarshal(new, v)) + require.Equal(t, int64(42), v.Votes) + + bad := []byte(`{"publickey":"02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62","votes":"notanumber"}`) + require.Error(t, json.Unmarshal(bad, v)) +} diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 491a4596b..8436369d8 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -118,6 +118,7 @@ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *respon "getblockheader": (*Server).getBlockHeader, "getblockheadercount": (*Server).getBlockHeaderCount, "getblocksysfee": (*Server).getBlockSysFee, + "getcandidates": (*Server).getCandidates, "getcommittee": (*Server).getCommittee, "getconnectioncount": (*Server).getConnectionCount, "getcontractstate": (*Server).getContractState, @@ -1538,6 +1539,29 @@ func (s *Server) getUnclaimedGas(ps request.Params) (interface{}, *response.Erro }, nil } +// getCandidates returns the current list of candidates with their active/inactive voting status. +func (s *Server) getCandidates(_ request.Params) (interface{}, *response.Error) { + var validators keys.PublicKeys + + validators, err := s.chain.GetNextBlockValidators() + if err != nil { + return nil, response.NewRPCError("Can't get next block validators", err.Error()) + } + enrollments, err := s.chain.GetEnrollments() + if err != nil { + return nil, response.NewRPCError("Can't get enrollments", err.Error()) + } + var res = make([]result.Candidate, 0) + for _, v := range enrollments { + res = append(res, result.Candidate{ + PublicKey: *v.Key, + Votes: v.Votes.Int64(), + Active: validators.Contains(v.Key), + }) + } + return res, nil +} + // getNextBlockValidators returns validators for the next block with voting status. func (s *Server) getNextBlockValidators(_ request.Params) (interface{}, *response.Error) { var validators keys.PublicKeys @@ -1552,10 +1576,12 @@ func (s *Server) getNextBlockValidators(_ request.Params) (interface{}, *respons } var res = make([]result.Validator, 0) for _, v := range enrollments { + if !validators.Contains(v.Key) { + continue + } res = append(res, result.Validator{ PublicKey: *v.Key, Votes: v.Votes.Int64(), - Active: validators.Contains(v.Key), }) } return res, nil diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 4ea259de8..27bf5985b 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -818,6 +818,14 @@ var rpcTestCases = map[string][]rpcTestCase{ }, }, }, + "getcandidates": { + { + params: "[]", + result: func(*executor) interface{} { + return &[]result.Candidate{} + }, + }, + }, "getnextblockvalidators": { { params: "[]",