From a1a5db8fcd1d7a6c13db79c5c1aaa81fb9201fab Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Aug 2022 10:54:29 +0300 Subject: [PATCH 1/4] state: add more convenient method to get native contract hashes --- pkg/core/interop/context.go | 2 +- pkg/core/state/contract.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index aefbb2839..85daf01ae 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -176,7 +176,7 @@ func NewContractMD(name string, id int32) *ContractMD { c.NEF.Header.Compiler = "neo-core-v3.0" c.NEF.Header.Magic = nef.Magic c.NEF.Tokens = []nef.MethodToken{} // avoid `nil` result during JSON marshalling - c.Hash = state.CreateContractHash(util.Uint160{}, 0, c.Name) + c.Hash = state.CreateNativeContractHash(c.Name) c.Manifest = *manifest.DefaultManifest(name) return c diff --git a/pkg/core/state/contract.go b/pkg/core/state/contract.go index 4bc5df70a..0b89c4b5b 100644 --- a/pkg/core/state/contract.go +++ b/pkg/core/state/contract.go @@ -115,3 +115,9 @@ func CreateContractHash(sender util.Uint160, checksum uint32, name string) util. } return hash.Hash160(w.Bytes()) } + +// CreateNativeContractHash calculates the hash for the native contract with the +// given name. +func CreateNativeContractHash(name string) util.Uint160 { + return CreateContractHash(util.Uint160{}, 0, name) +} From ee84a4ab32a1677fc760819253714cd274bb11ea Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Aug 2022 16:07:23 +0300 Subject: [PATCH 2/4] 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() From ee72b2fa2923b73d391d5db11131b62d2edf821e Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Aug 2022 16:42:21 +0300 Subject: [PATCH 3/4] rpcclient: add gas package for the GAS contract Test it with the RPC server. --- cli/wallet/nep17.go | 6 ++++- pkg/rpcclient/gas/gas.go | 26 +++++++++++++++++++ pkg/rpcclient/gas/gas_test.go | 40 ++++++++++++++++++++++++++++++ pkg/services/rpcsrv/client_test.go | 19 ++++++-------- 4 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 pkg/rpcclient/gas/gas.go create mode 100644 pkg/rpcclient/gas/gas_test.go diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index f7596945b..0ef3463dc 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -17,6 +17,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" "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/smartcontract/manifest" @@ -241,7 +242,8 @@ func getNEP17Balance(ctx *cli.Context) error { if err != nil { // Try to get native NEP17 with matching symbol. var gasSymbol, neoSymbol string - gasSymbol, h, err = getNativeNEP17Symbol(c, nativenames.Gas) + g := gas.NewReader(invoker.New(c, nil)) + gasSymbol, err = g.Symbol() if err != nil { continue } @@ -253,6 +255,8 @@ func getNEP17Balance(ctx *cli.Context) error { if neoSymbol != name { continue } + } else { + h = gas.Hash } } } diff --git a/pkg/rpcclient/gas/gas.go b/pkg/rpcclient/gas/gas.go new file mode 100644 index 000000000..a19dde6bc --- /dev/null +++ b/pkg/rpcclient/gas/gas.go @@ -0,0 +1,26 @@ +/* +Package gas provides a convenience wrapper for GAS contract to use it via RPC. + +GAS itself only has standard NEP-17 methods, so this package only contains its +hash and allows to create NEP-17 structures in an easier way. +*/ +package gas + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" +) + +// Hash stores the hash of the native GAS contract. +var Hash = state.CreateNativeContractHash(nativenames.Gas) + +// NewReader creates a NEP-17 reader for the GAS contract. +func NewReader(invoker nep17.Invoker) *nep17.TokenReader { + return nep17.NewReader(invoker, Hash) +} + +// New creates a NEP-17 contract instance for the native GAS contract. +func New(actor nep17.Actor) *nep17.Token { + return nep17.New(actor, Hash) +} diff --git a/pkg/rpcclient/gas/gas_test.go b/pkg/rpcclient/gas/gas_test.go new file mode 100644 index 000000000..eb49affda --- /dev/null +++ b/pkg/rpcclient/gas/gas_test.go @@ -0,0 +1,40 @@ +package gas + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/util" + "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) MakeRun(script []byte) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) SendRun(script []byte) (util.Uint256, uint32, error) { + return t.txh, t.vub, t.err +} + +func TestNew(t *testing.T) { + ta := &testAct{} + gr := NewReader(ta) + require.NotNil(t, gr) + + g := New(ta) + require.NotNil(t, g) +} diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index e6d54d6a2..116f02ab3 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -20,7 +20,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config" "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" @@ -31,6 +30,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/network" "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" "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" @@ -781,14 +781,11 @@ func TestCreateNEP17TransferTx(t *testing.T) { acc := wallet.NewAccountFromPrivateKey(priv) addr := priv.PublicKey().GetScriptHash() - gasContractHash, err := c.GetNativeContractHash(nativenames.Gas) - require.NoError(t, err) - t.Run("default scope", func(t *testing.T) { act, err := actor.NewSimple(c, acc) require.NoError(t, err) - gas := nep17.New(act, gasContractHash) - tx, err := gas.TransferUnsigned(addr, util.Uint160{}, big.NewInt(1000), nil) + gasprom := gas.New(act) + tx, err := gasprom.TransferUnsigned(addr, util.Uint160{}, big.NewInt(1000), nil) require.NoError(t, err) require.NoError(t, acc.SignTx(testchain.Network(), tx)) require.NoError(t, chain.VerifyTx(tx)) @@ -805,8 +802,8 @@ func TestCreateNEP17TransferTx(t *testing.T) { Account: acc, }}) require.NoError(t, err) - gas := nep17.New(act, gasContractHash) - _, err = gas.TransferUnsigned(addr, util.Uint160{}, big.NewInt(1000), nil) + gasprom := gas.New(act) + _, err = gasprom.TransferUnsigned(addr, util.Uint160{}, big.NewInt(1000), nil) require.Error(t, err) }) t.Run("customcontracts scope", func(t *testing.T) { @@ -814,13 +811,13 @@ func TestCreateNEP17TransferTx(t *testing.T) { Signer: transaction.Signer{ Account: priv.PublicKey().GetScriptHash(), Scopes: transaction.CustomContracts, - AllowedContracts: []util.Uint160{gasContractHash}, + AllowedContracts: []util.Uint160{gas.Hash}, }, Account: acc, }}) require.NoError(t, err) - gas := nep17.New(act, gasContractHash) - tx, err := gas.TransferUnsigned(addr, util.Uint160{}, big.NewInt(1000), nil) + gasprom := gas.New(act) + tx, err := gasprom.TransferUnsigned(addr, util.Uint160{}, big.NewInt(1000), nil) require.NoError(t, err) require.NoError(t, acc.SignTx(testchain.Network(), tx)) require.NoError(t, chain.VerifyTx(tx)) From 5d5455312a12b1adf0e4c780632643aeffa1f0f0 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Aug 2022 18:44:12 +0300 Subject: [PATCH 4/4] rpcclient: add policy package for the PolicyContract contract And test it with the RPC server. Notice that getters still return int64 instead of *big.Int, that's because these values are very limited and technically could even fit into an int (but that seems to be too dangerous to use for long-term compatibility). --- pkg/rpcclient/policy.go | 8 + pkg/rpcclient/policy/policy.go | 219 ++++++++++++++++++++++++++++ pkg/rpcclient/policy/policy_test.go | 182 +++++++++++++++++++++++ pkg/services/rpcsrv/client_test.go | 81 ++++++++++ 4 files changed, 490 insertions(+) create mode 100644 pkg/rpcclient/policy/policy.go create mode 100644 pkg/rpcclient/policy/policy_test.go diff --git a/pkg/rpcclient/policy.go b/pkg/rpcclient/policy.go index e6a7f1733..84e386a29 100644 --- a/pkg/rpcclient/policy.go +++ b/pkg/rpcclient/policy.go @@ -9,16 +9,22 @@ import ( ) // GetFeePerByte invokes `getFeePerByte` method on a native Policy contract. +// +// Deprecated: please use policy subpackage. func (c *Client) GetFeePerByte() (int64, error) { return c.invokeNativePolicyMethod("getFeePerByte") } // GetExecFeeFactor invokes `getExecFeeFactor` method on a native Policy contract. +// +// Deprecated: please use policy subpackage. func (c *Client) GetExecFeeFactor() (int64, error) { return c.invokeNativePolicyMethod("getExecFeeFactor") } // GetStoragePrice invokes `getStoragePrice` method on a native Policy contract. +// +// Deprecated: please use policy subpackage. func (c *Client) GetStoragePrice() (int64, error) { return c.invokeNativePolicyMethod("getStoragePrice") } @@ -46,6 +52,8 @@ func (c *Client) invokeNativeGetMethod(hash util.Uint160, operation string) (int } // IsBlocked invokes `isBlocked` method on native Policy contract. +// +// Deprecated: please use policy subpackage. func (c *Client) IsBlocked(hash util.Uint160) (bool, error) { policyHash, err := c.GetNativeContractHash(nativenames.Policy) if err != nil { diff --git a/pkg/rpcclient/policy/policy.go b/pkg/rpcclient/policy/policy.go new file mode 100644 index 000000000..2f583db0a --- /dev/null +++ b/pkg/rpcclient/policy/policy.go @@ -0,0 +1,219 @@ +/* +Package policy allows to work with the native PolicyContract contract via RPC. + +Safe methods are encapsulated into ContractReader structure while Contract provides +various methods to perform PolicyContract state-changing calls. +*/ +package policy + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "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/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "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) + MakeRun(script []byte) (*transaction.Transaction, error) + MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error) + MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) + SendCall(contract util.Uint160, method string, params ...interface{}) (util.Uint256, uint32, error) + SendRun(script []byte) (util.Uint256, uint32, error) +} + +// Hash stores the hash of the native PolicyContract contract. +var Hash = state.CreateNativeContractHash(nativenames.Policy) + +const ( + execFeeSetter = "setExecFeeFactor" + feePerByteSetter = "setFeePerByte" + storagePriceSetter = "setStoragePrice" +) + +// ContractReader provides an interface to call read-only PolicyContract +// contract's methods. +type ContractReader struct { + invoker Invoker +} + +// Contract represents a PolicyContract contract client that can be used to +// invoke all of its methods. +type Contract struct { + ContractReader + + actor Actor +} + +// 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 PolicyContract's state can be changed +// only by the network's committee, so the Actor provided must be a committee +// actor for all methods to work properly. +func New(actor Actor) *Contract { + return &Contract{*NewReader(actor), actor} +} + +// GetExecFeeFactor returns current execution fee factor used by the network. +// This setting affects all executions of all transactions. +func (c *ContractReader) GetExecFeeFactor() (int64, error) { + return unwrap.Int64(c.invoker.Call(Hash, "getExecFeeFactor")) +} + +// GetFeePerByte returns current minimal per-byte network fee value which +// affects all transactions on the network. +func (c *ContractReader) GetFeePerByte() (int64, error) { + return unwrap.Int64(c.invoker.Call(Hash, "getFeePerByte")) +} + +// GetStoragePrice returns current per-byte storage price. Any contract saving +// data to the storage pays for it according to this value. +func (c *ContractReader) GetStoragePrice() (int64, error) { + return unwrap.Int64(c.invoker.Call(Hash, "getStoragePrice")) +} + +// IsBlocked checks if the given account is blocked in the PolicyContract. +func (c *ContractReader) IsBlocked(account util.Uint160) (bool, error) { + return unwrap.Bool(c.invoker.Call(Hash, "isBlocked", account)) +} + +// SetExecFeeFactor creates and sends a transaction that sets the new +// execution fee factor for the network to use. 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) SetExecFeeFactor(value int64) (util.Uint256, uint32, error) { + return c.actor.SendCall(Hash, execFeeSetter, value) +} + +// SetExecFeeFactorTransaction creates a transaction that sets the new execution +// fee factor. This transaction is signed, but not sent to the network, +// instead it's returned to the caller. +func (c *Contract) SetExecFeeFactorTransaction(value int64) (*transaction.Transaction, error) { + return c.actor.MakeCall(Hash, execFeeSetter, value) +} + +// SetExecFeeFactorUnsigned creates a transaction that sets the new execution +// fee factor. This transaction is not signed and just returned to the caller. +func (c *Contract) SetExecFeeFactorUnsigned(value int64) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(Hash, execFeeSetter, nil, value) +} + +// SetFeePerByte creates and sends a transaction that sets the new minimal +// per-byte network fee value. 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) SetFeePerByte(value int64) (util.Uint256, uint32, error) { + return c.actor.SendCall(Hash, feePerByteSetter, value) +} + +// SetFeePerByteTransaction creates a transaction that sets the new minimal +// per-byte network fee value. This transaction is signed, but not sent to the +// network, instead it's returned to the caller. +func (c *Contract) SetFeePerByteTransaction(value int64) (*transaction.Transaction, error) { + return c.actor.MakeCall(Hash, feePerByteSetter, value) +} + +// SetFeePerByteUnsigned creates a transaction that sets the new minimal per-byte +// network fee value. This transaction is not signed and just returned to the +// caller. +func (c *Contract) SetFeePerByteUnsigned(value int64) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(Hash, feePerByteSetter, nil, value) +} + +// SetStoragePrice creates and sends a transaction that sets the storage price +// for contracts. 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) SetStoragePrice(value int64) (util.Uint256, uint32, error) { + return c.actor.SendCall(Hash, storagePriceSetter, value) +} + +// SetStoragePriceTransaction creates a transaction that sets the storage price +// for contracts. This transaction is signed, but not sent to the network, +// instead it's returned to the caller. +func (c *Contract) SetStoragePriceTransaction(value int64) (*transaction.Transaction, error) { + return c.actor.MakeCall(Hash, storagePriceSetter, value) +} + +// SetStoragePriceUnsigned creates a transaction that sets the storage price +// for contracts. This transaction is not signed and just returned to the +// caller. +func (c *Contract) SetStoragePriceUnsigned(value int64) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(Hash, storagePriceSetter, nil, value) +} + +// BlockAccount creates and sends a transaction that blocks an account on the +// network (via `blockAccount` method), it fails (with FAULT state) if it's not +// successful. The returned values are transaction hash, its +// ValidUntilBlock value and an error if any. +func (c *Contract) BlockAccount(account util.Uint160) (util.Uint256, uint32, error) { + return c.actor.SendRun(blockScript(account)) +} + +// BlockAccountTransaction creates a transaction that blocks an account on the +// network and checks for the result of the appropriate call, failing the +// transaction if it's not true. This transaction is signed, but not sent to the +// network, instead it's returned to the caller. +func (c *Contract) BlockAccountTransaction(account util.Uint160) (*transaction.Transaction, error) { + return c.actor.MakeRun(blockScript(account)) +} + +// BlockAccountUnsigned creates a transaction that blocks an account on the +// network and checks for the result of the appropriate call, failing the +// transaction if it's not true. This transaction is not signed and just returned +// to the caller. +func (c *Contract) BlockAccountUnsigned(account util.Uint160) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedRun(blockScript(account), nil) +} + +func blockScript(account util.Uint160) []byte { + // We know parameters exactly (unlike with nep17.Transfer), so this can't fail. + script, _ := smartcontract.CreateCallWithAssertScript(Hash, "blockAccount", account) + return script +} + +// UnblockAccount creates and sends a transaction that removes previously blocked +// account from the stop list. It uses `unblockAccount` method and checks for the +// result returned, failing the transaction if it's not true. The returned values +// are transaction hash, its ValidUntilBlock value and an error if any. +func (c *Contract) UnblockAccount(account util.Uint160) (util.Uint256, uint32, error) { + return c.actor.SendRun(unblockScript(account)) +} + +// UnblockAccountTransaction creates a transaction that unblocks previously +// blocked account via `unblockAccount` method and checks for the result returned, +// failing the transaction if it's not true. This transaction is signed, but not +// sent to the network, instead it's returned to the caller. +func (c *Contract) UnblockAccountTransaction(account util.Uint160) (*transaction.Transaction, error) { + return c.actor.MakeRun(unblockScript(account)) +} + +// UnblockAccountUnsigned creates a transaction that unblocks the given account +// if it was blocked previously. It uses `unblockAccount` method and checks for +// its return value, failing the transaction if it's not true. This transaction +// is not signed and just returned to the caller. +func (c *Contract) UnblockAccountUnsigned(account util.Uint160) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedRun(unblockScript(account), nil) +} + +func unblockScript(account util.Uint160) []byte { + // We know parameters exactly (unlike with nep17.Transfer), so this can't fail. + script, _ := smartcontract.CreateCallWithAssertScript(Hash, "unblockAccount", account) + return script +} diff --git a/pkg/rpcclient/policy/policy_test.go b/pkg/rpcclient/policy/policy_test.go new file mode 100644 index 000000000..aed3dcd16 --- /dev/null +++ b/pkg/rpcclient/policy/policy_test.go @@ -0,0 +1,182 @@ +package policy + +import ( + "errors" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "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 (t *testAct) MakeRun(script []byte) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) SendRun(script []byte) (util.Uint256, uint32, error) { + return t.txh, t.vub, t.err +} + +func TestReader(t *testing.T) { + ta := new(testAct) + pc := NewReader(ta) + + meth := []func() (int64, error){ + pc.GetExecFeeFactor, + pc.GetFeePerByte, + pc.GetStoragePrice, + } + + ta.err = errors.New("") + for _, m := range meth { + _, err := m() + require.Error(t, err) + } + _, err := pc.IsBlocked(util.Uint160{1, 2, 3}) + require.Error(t, err) + + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(42), + }, + } + for _, m := range meth { + val, err := m() + require.NoError(t, err) + require.Equal(t, int64(42), val) + } + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(true), + }, + } + val, err := pc.IsBlocked(util.Uint160{1, 2, 3}) + require.NoError(t, err) + require.True(t, val) +} + +func TestIntSetters(t *testing.T) { + ta := new(testAct) + pc := New(ta) + + meth := []func(int64) (util.Uint256, uint32, error){ + pc.SetExecFeeFactor, + pc.SetFeePerByte, + pc.SetStoragePrice, + } + + ta.err = errors.New("") + for _, m := range meth { + _, _, err := m(42) + require.Error(t, err) + } + + ta.err = nil + ta.txh = util.Uint256{1, 2, 3} + ta.vub = 42 + for _, m := range meth { + h, vub, err := m(100) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) + } +} + +func TestUint160Setters(t *testing.T) { + ta := new(testAct) + pc := New(ta) + + meth := []func(util.Uint160) (util.Uint256, uint32, error){ + pc.BlockAccount, + pc.UnblockAccount, + } + + ta.err = errors.New("") + for _, m := range meth { + _, _, err := m(util.Uint160{}) + require.Error(t, err) + } + + ta.err = nil + ta.txh = util.Uint256{1, 2, 3} + ta.vub = 42 + for _, m := range meth { + h, vub, err := m(util.Uint160{}) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) + } +} + +func TestIntTransactions(t *testing.T) { + ta := new(testAct) + pc := New(ta) + + for _, fun := range []func(int64) (*transaction.Transaction, error){ + pc.SetExecFeeFactorTransaction, + pc.SetExecFeeFactorUnsigned, + pc.SetFeePerByteTransaction, + pc.SetFeePerByteUnsigned, + pc.SetStoragePriceTransaction, + pc.SetStoragePriceUnsigned, + } { + ta.err = errors.New("") + _, err := fun(1) + require.Error(t, err) + + ta.err = nil + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := fun(1) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + } +} + +func TestUint160Transactions(t *testing.T) { + ta := new(testAct) + pc := New(ta) + + for _, fun := range []func(util.Uint160) (*transaction.Transaction, error){ + pc.BlockAccountTransaction, + pc.BlockAccountUnsigned, + pc.UnblockAccountTransaction, + pc.UnblockAccountUnsigned, + } { + ta.err = errors.New("") + _, err := fun(util.Uint160{1}) + require.Error(t, err) + + ta.err = nil + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := fun(util.Uint160{1}) + 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 116f02ab3..2882b5083 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -34,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/policy" "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" @@ -144,6 +145,86 @@ func TestClientRoleManagement(t *testing.T) { require.Equal(t, testKeys, ks) } +func TestClientPolicyContract(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()) + + polizei := policy.NewReader(invoker.New(c, nil)) + + val, err := polizei.GetExecFeeFactor() + require.NoError(t, err) + require.Equal(t, int64(30), val) + + val, err = polizei.GetFeePerByte() + require.NoError(t, err) + require.Equal(t, int64(1000), val) + + val, err = polizei.GetStoragePrice() + require.NoError(t, err) + require.Equal(t, int64(100000), val) + + ret, err := polizei.IsBlocked(util.Uint160{}) + require.NoError(t, err) + require.False(t, ret) + + 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) + + polis := policy.New(act) + + txexec, err := polis.SetExecFeeFactorUnsigned(100) + require.NoError(t, err) + + txnetfee, err := polis.SetFeePerByteUnsigned(500) + require.NoError(t, err) + + txstorage, err := polis.SetStoragePriceUnsigned(100500) + require.NoError(t, err) + + txblock, err := polis.BlockAccountUnsigned(util.Uint160{1, 2, 3}) + require.NoError(t, err) + + for _, tx := range []*transaction.Transaction{txblock, txstorage, txnetfee, txexec} { + tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx) + } + + bl := testchain.NewBlock(t, chain, 1, 0, txblock, txstorage, txnetfee, txexec) + _, err = c.SubmitBlock(*bl) + require.NoError(t, err) + + val, err = polizei.GetExecFeeFactor() + require.NoError(t, err) + require.Equal(t, int64(100), val) + + val, err = polizei.GetFeePerByte() + require.NoError(t, err) + require.Equal(t, int64(500), val) + + val, err = polizei.GetStoragePrice() + require.NoError(t, err) + require.Equal(t, int64(100500), val) + + ret, err = polizei.IsBlocked(util.Uint160{1, 2, 3}) + require.NoError(t, err) + require.True(t, ret) +} + func TestAddNetworkFeeCalculateNetworkFee(t *testing.T) { chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) defer chain.Close()