From ee84a4ab32a1677fc760819253714cd274bb11ea Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Aug 2022 16:07:23 +0300 Subject: [PATCH] rpcclient: add rolemgmt pkg for RoleManagement contract And test it with RPC server. --- pkg/rpcclient/native.go | 2 + pkg/rpcclient/rolemgmt/roles.go | 119 +++++++++++++++++++++ pkg/rpcclient/rolemgmt/roles_test.go | 151 +++++++++++++++++++++++++++ pkg/services/rpcsrv/client_test.go | 54 ++++++++++ 4 files changed, 326 insertions(+) create mode 100644 pkg/rpcclient/rolemgmt/roles.go create mode 100644 pkg/rpcclient/rolemgmt/roles_test.go diff --git a/pkg/rpcclient/native.go b/pkg/rpcclient/native.go index 609f353fa..827f23d62 100644 --- a/pkg/rpcclient/native.go +++ b/pkg/rpcclient/native.go @@ -52,6 +52,8 @@ func (c *Client) getFromNEO(meth string) (int64, error) { } // GetDesignatedByRole invokes `getDesignatedByRole` method on a native RoleManagement contract. +// +// Deprecated: please use rolemgmt package. func (c *Client) GetDesignatedByRole(role noderoles.Role, index uint32) (keys.PublicKeys, error) { rmHash, err := c.GetNativeContractHash(nativenames.Designation) if err != nil { diff --git a/pkg/rpcclient/rolemgmt/roles.go b/pkg/rpcclient/rolemgmt/roles.go new file mode 100644 index 000000000..d153feb9c --- /dev/null +++ b/pkg/rpcclient/rolemgmt/roles.go @@ -0,0 +1,119 @@ +/* +Package rolemgmt allows to work with the native RoleManagement contract via RPC. + +Safe methods are encapsulated into ContractReader structure while Contract provides +various methods to perform the only RoleManagement state-changing call. +*/ +package rolemgmt + +import ( + "crypto/elliptic" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// Invoker is used by ContractReader to call various methods. +type Invoker interface { + Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) +} + +// Actor is used by Contract to create and send transactions. +type Actor interface { + Invoker + + MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error) + MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error) + SendCall(contract util.Uint160, method string, params ...interface{}) (util.Uint256, uint32, error) +} + +// Hash stores the hash of the native RoleManagement contract. +var Hash = state.CreateNativeContractHash(nativenames.Designation) + +const designateMethod = "designateAsRole" + +// ContractReader provides an interface to call read-only RoleManagement +// contract's methods. +type ContractReader struct { + invoker Invoker +} + +// Contract represents a RoleManagement contract client that can be used to +// invoke all of its methods. +type Contract struct { + ContractReader + + actor Actor +} + +// DesignationEvent represents an event emitted by RoleManagement contract when +// a new role designation is done. +type DesignationEvent struct { + Role noderoles.Role + BlockIndex uint32 +} + +// NewReader creates an instance of ContractReader that can be used to read +// data from the contract. +func NewReader(invoker Invoker) *ContractReader { + return &ContractReader{invoker} +} + +// New creates an instance of Contract to perform actions using +// the given Actor. Notice that RoleManagement's state can be changed +// only by the network's committee, so the Actor provided must be a committee +// actor for designation methods to work properly. +func New(actor Actor) *Contract { + return &Contract{*NewReader(actor), actor} +} + +// GetDesignatedByRole returns the list of the keys designated to serve for the +// given role at the given height. The list can be empty if no keys are +// configured for this role/height. +func (c *ContractReader) GetDesignatedByRole(role noderoles.Role, index uint32) (keys.PublicKeys, error) { + arr, err := unwrap.Array(c.invoker.Call(Hash, "getDesignatedByRole", int64(role), index)) + if err != nil { + return nil, err + } + pks := make(keys.PublicKeys, len(arr)) + for i, item := range arr { + val, err := item.TryBytes() + if err != nil { + return nil, fmt.Errorf("invalid array element #%d: %s", i, item.Type()) + } + pks[i], err = keys.NewPublicKeyFromBytes(val, elliptic.P256()) + if err != nil { + return nil, err + } + } + return pks, nil +} + +// DesignateAsRole creates and sends a transaction that sets the keys used for +// the given node role. The action is successful when transaction ends in HALT +// state. The returned values are transaction hash, its ValidUntilBlock value +// and an error if any. +func (c *Contract) DesignateAsRole(role noderoles.Role, pubs keys.PublicKeys) (util.Uint256, uint32, error) { + return c.actor.SendCall(Hash, designateMethod, int(role), pubs) +} + +// DesignateAsRoleTransaction creates a transaction that sets the keys for the +// given node role. This transaction is signed, but not sent to the network, +// instead it's returned to the caller. +func (c *Contract) DesignateAsRoleTransaction(role noderoles.Role, pubs keys.PublicKeys) (*transaction.Transaction, error) { + return c.actor.MakeCall(Hash, designateMethod, int(role), pubs) +} + +// DesignateAsRoleUnsigned creates a transaction that sets the keys for the +// given node role. This transaction is not signed and just returned to the +// caller. +func (c *Contract) DesignateAsRoleUnsigned(role noderoles.Role, pubs keys.PublicKeys) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(Hash, designateMethod, nil, int(role), pubs) +} diff --git a/pkg/rpcclient/rolemgmt/roles_test.go b/pkg/rpcclient/rolemgmt/roles_test.go new file mode 100644 index 000000000..f1899ed80 --- /dev/null +++ b/pkg/rpcclient/rolemgmt/roles_test.go @@ -0,0 +1,151 @@ +package rolemgmt + +import ( + "errors" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +type testAct struct { + err error + res *result.Invoke + tx *transaction.Transaction + txh util.Uint256 + vub uint32 +} + +func (t *testAct) Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) { + return t.res, t.err +} +func (t *testAct) MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) SendCall(contract util.Uint160, method string, params ...interface{}) (util.Uint256, uint32, error) { + return t.txh, t.vub, t.err +} + +func TestReaderGetDesignatedByRole(t *testing.T) { + ta := new(testAct) + rc := NewReader(ta) + + ta.err = errors.New("") + _, err := rc.GetDesignatedByRole(noderoles.Oracle, 0) + require.Error(t, err) + + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(100500), + }, + } + _, err = rc.GetDesignatedByRole(noderoles.Oracle, 0) + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Null{}, + }, + } + _, err = rc.GetDesignatedByRole(noderoles.Oracle, 0) + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{}), + }, + } + nodes, err := rc.GetDesignatedByRole(noderoles.Oracle, 0) + require.NoError(t, err) + require.NotNil(t, nodes) + require.Equal(t, 0, len(nodes)) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{stackitem.Null{}}), + }, + } + _, err = rc.GetDesignatedByRole(noderoles.Oracle, 0) + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{stackitem.Make(42)}), + }, + } + _, err = rc.GetDesignatedByRole(noderoles.Oracle, 0) + require.Error(t, err) + + k, err := keys.NewPrivateKey() + require.NoError(t, err) + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{stackitem.Make(k.PublicKey().Bytes())}), + }, + } + nodes, err = rc.GetDesignatedByRole(noderoles.Oracle, 0) + require.NoError(t, err) + require.NotNil(t, nodes) + require.Equal(t, 1, len(nodes)) + require.Equal(t, k.PublicKey(), nodes[0]) +} + +func TestDesignateAsRole(t *testing.T) { + ta := new(testAct) + rc := New(ta) + + k, err := keys.NewPrivateKey() + require.NoError(t, err) + ks := keys.PublicKeys{k.PublicKey()} + + ta.err = errors.New("") + _, _, err = rc.DesignateAsRole(noderoles.Oracle, ks) + require.Error(t, err) + + ta.err = nil + ta.txh = util.Uint256{1, 2, 3} + ta.vub = 42 + h, vub, err := rc.DesignateAsRole(noderoles.Oracle, ks) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) +} + +func TestDesignateAsRoleTransaction(t *testing.T) { + ta := new(testAct) + rc := New(ta) + + k, err := keys.NewPrivateKey() + require.NoError(t, err) + ks := keys.PublicKeys{k.PublicKey()} + + for _, fun := range []func(r noderoles.Role, pubs keys.PublicKeys) (*transaction.Transaction, error){ + rc.DesignateAsRoleTransaction, + rc.DesignateAsRoleUnsigned, + } { + ta.err = errors.New("") + _, err := fun(noderoles.P2PNotary, ks) + require.Error(t, err) + + ta.err = nil + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := fun(noderoles.P2PNotary, ks) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + } +} diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index ef11a0f7b..e6d54d6a2 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -21,6 +21,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -33,6 +34,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nns" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" @@ -90,6 +92,58 @@ func TestClient_NEP17(t *testing.T) { }) } +func TestClientRoleManagement(t *testing.T) { + chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) + defer chain.Close() + defer rpcSrv.Shutdown() + + c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) + require.NoError(t, err) + require.NoError(t, c.Init()) + + act, err := actor.New(c, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: &wallet.Account{ + Address: testchain.CommitteeAddress(), + Contract: &wallet.Contract{ + Script: testchain.CommitteeVerificationScript(), + }, + }, + }}) + require.NoError(t, err) + + height, err := c.GetBlockCount() + require.NoError(t, err) + + rm := rolemgmt.New(act) + ks, err := rm.GetDesignatedByRole(noderoles.Oracle, height) + require.NoError(t, err) + require.Equal(t, 0, len(ks)) + + testKeys := keys.PublicKeys{ + testchain.PrivateKeyByID(0).PublicKey(), + testchain.PrivateKeyByID(1).PublicKey(), + testchain.PrivateKeyByID(2).PublicKey(), + testchain.PrivateKeyByID(3).PublicKey(), + } + + tx, err := rm.DesignateAsRoleUnsigned(noderoles.Oracle, testKeys) + require.NoError(t, err) + + tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx) + bl := testchain.NewBlock(t, chain, 1, 0, tx) + _, err = c.SubmitBlock(*bl) + require.NoError(t, err) + + sort.Sort(testKeys) + ks, err = rm.GetDesignatedByRole(noderoles.Oracle, height+1) + require.NoError(t, err) + require.Equal(t, testKeys, ks) +} + func TestAddNetworkFeeCalculateNetworkFee(t *testing.T) { chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) defer chain.Close()