From 5d5455312a12b1adf0e4c780632643aeffa1f0f0 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Aug 2022 18:44:12 +0300 Subject: [PATCH] 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()