From c0705e45c9a0215eb7a502fe47a181de91e1cbd0 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 2 Aug 2022 12:40:12 +0300 Subject: [PATCH] rpcclient: add actor package Somewhat similar to invoker, but changing the state (or just creating a transaction). Transaction creation could've been put into a structure of its own, but it seems to be less convenient to use this way. --- pkg/rpcclient/actor/actor.go | 204 ++++++++++++++++++++++++ pkg/rpcclient/actor/actor_test.go | 254 ++++++++++++++++++++++++++++++ pkg/rpcclient/actor/maker.go | 195 +++++++++++++++++++++++ pkg/rpcclient/actor/maker_test.go | 161 +++++++++++++++++++ pkg/rpcclient/rpc.go | 3 + 5 files changed, 817 insertions(+) create mode 100644 pkg/rpcclient/actor/actor.go create mode 100644 pkg/rpcclient/actor/actor_test.go create mode 100644 pkg/rpcclient/actor/maker.go create mode 100644 pkg/rpcclient/actor/maker_test.go diff --git a/pkg/rpcclient/actor/actor.go b/pkg/rpcclient/actor/actor.go new file mode 100644 index 000000000..abf79530f --- /dev/null +++ b/pkg/rpcclient/actor/actor.go @@ -0,0 +1,204 @@ +/* +Package actor provides a way to change chain state via RPC client. + +This layer builds on top of the basic RPC client and simplifies creating, +signing and sending transactions to the network (since that's the only way chain +state is changed). It's generic enough to be used for any contract that you may +want to invoke and contract-specific functions can build on top of it. +*/ +package actor + +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "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/invoker" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +// RPCActor is an interface required from the RPC client to successfully +// create and send transactions. +type RPCActor interface { + invoker.RPCInvoke + + CalculateNetworkFee(tx *transaction.Transaction) (int64, error) + GetBlockCount() (uint32, error) + GetVersion() (*result.Version, error) + SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) +} + +// SignerAccount represents combination of the transaction.Signer and the +// corresponding wallet.Account. It's used to create and sign transactions, each +// transaction has a set of signers that must witness the transaction with their +// signatures. +type SignerAccount struct { + Signer transaction.Signer + Account *wallet.Account +} + +// Actor keeps a connection to the RPC endpoint and allows to perform +// state-changing actions (via transactions that can also be created without +// sending them to the network) on behalf of a set of signers. It also provides +// an Invoker interface to perform test calls with the same set of signers. +type Actor struct { + invoker.Invoker + + client RPCActor + signers []SignerAccount + txSigners []transaction.Signer + version *result.Version +} + +// New creates an Actor instance using the specified RPC interface and the set of +// signers with corresponding accounts. Every transaction created by this Actor +// will have this set of signers and all communication will be performed via this +// RPC. Upon Actor instance creation a GetVersion call is made and the result of +// it is cached forever (and used for internal purposes). +func New(ra RPCActor, signers []SignerAccount) (*Actor, error) { + if len(signers) < 1 { + return nil, errors.New("at least one signer (sender) is required") + } + invSigners := make([]transaction.Signer, len(signers)) + for i := range signers { + if signers[i].Account.Contract == nil { + return nil, fmt.Errorf("empty contract for account %s", signers[i].Account.Address) + } + if !signers[i].Account.Contract.Deployed && signers[i].Account.Contract.ScriptHash() != signers[i].Signer.Account { + return nil, fmt.Errorf("signer account doesn't match script hash for signer %s", signers[i].Account.Address) + } + + invSigners[i] = signers[i].Signer + } + inv := invoker.New(ra, invSigners) + version, err := ra.GetVersion() + if err != nil { + return nil, err + } + return &Actor{ + Invoker: *inv, + client: ra, + signers: signers, + txSigners: invSigners, + version: version, + }, nil +} + +// NewSimple makes it easier to create an Actor for the most widespread case +// when transactions have only one signer that uses CalledByEntry scope. When +// other scopes or multiple signers are needed use New. +func NewSimple(ra RPCActor, acc *wallet.Account) (*Actor, error) { + return New(ra, []SignerAccount{{ + Signer: transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: acc, + }}) +} + +// CalculateNetworkFee wraps RPCActor's CalculateNetworkFee, making it available +// to Actor users directly. It returns network fee value for the given +// transaction. +func (a *Actor) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) { + return a.client.CalculateNetworkFee(tx) +} + +// GetBlockCount wraps RPCActor's GetBlockCount, making it available to +// Actor users directly. It returns current number of blocks in the chain. +func (a *Actor) GetBlockCount() (uint32, error) { + return a.client.GetBlockCount() +} + +// GetNetwork is a convenience method that returns the network's magic number. +func (a *Actor) GetNetwork() netmode.Magic { + return a.version.Protocol.Network +} + +// GetVersion returns version data from the RPC endpoint. +func (a *Actor) GetVersion() result.Version { + return *a.version +} + +// Send allows to send arbitrary prepared transaction to the network. It returns +// transaction hash and ValidUntilBlock value. +func (a *Actor) Send(tx *transaction.Transaction) (util.Uint256, uint32, error) { + h, err := a.client.SendRawTransaction(tx) + return h, tx.ValidUntilBlock, err +} + +// Sign adds signatures to arbitrary transaction using Actor signers wallets. +// Most of the time it shouldn't be used directly since it'll be successful only +// if the transaction is made using the same set of accounts as the one used +// for Actor creation. +func (a *Actor) Sign(tx *transaction.Transaction) error { + if len(tx.Signers) != len(a.signers) { + return errors.New("incorrect number of signers in the transaction") + } + for i, signer := range a.signers { + err := signer.Account.SignTx(a.GetNetwork(), tx) + if err != nil { // then account is non-contract-based and locked, but let's provide more detailed error + if paramNum := len(signer.Account.Contract.Parameters); paramNum != 0 && signer.Account.Contract.Deployed { + return fmt.Errorf("failed to add contract-based witness for signer #%d (%s): "+ + "%d parameters must be provided to construct invocation script", i, signer.Account.Address, paramNum) + } + return fmt.Errorf("failed to add witness for signer #%d (%s): account should be unlocked to add the signature. "+ + "Store partially-signed transaction and then use 'wallet sign' command to cosign it", i, signer.Account.Address) + } + } + return nil +} + +// SignAndSend signs arbitrary transaction (see also Sign) and sends it to the +// network. +func (a *Actor) SignAndSend(tx *transaction.Transaction) (util.Uint256, uint32, error) { + return a.sendWrapper(tx, a.Sign(tx)) +} + +// sendWrapper simplifies wrapping methods that create transactions. +func (a *Actor) sendWrapper(tx *transaction.Transaction, err error) (util.Uint256, uint32, error) { + if err != nil { + return util.Uint256{}, 0, err + } + return a.Send(tx) +} + +// SendCall creates a transaction that calls the given method of the given +// contract with the given parameters (see also MakeCall) and sends it to the +// network. +func (a *Actor) SendCall(contract util.Uint160, method string, params ...interface{}) (util.Uint256, uint32, error) { + return a.sendWrapper(a.MakeCall(contract, method, params...)) +} + +// SendTunedCall creates a transaction that calls the given method of the given +// contract with the given parameters (see also MakeTunedCall) and attributes, +// allowing to check for execution results of this call and modify transaction +// before it's signed; this transaction is then sent to the network. +func (a *Actor) SendTunedCall(contract util.Uint160, method string, attrs []transaction.Attribute, txHook TransactionCheckerModifier, params ...interface{}) (util.Uint256, uint32, error) { + return a.sendWrapper(a.MakeTunedCall(contract, method, attrs, txHook, params...)) +} + +// SendRun creates a transaction with the given executable script (see also +// MakeRun) and sends it to the network. +func (a *Actor) SendRun(script []byte) (util.Uint256, uint32, error) { + return a.sendWrapper(a.MakeRun(script)) +} + +// SendTunedRun creates a transaction with the given executable script and +// attributes, allowing to check for execution results of this script and modify +// transaction before it's signed (see also MakeTunedRun). This transaction is +// then sent to the network. +func (a *Actor) SendTunedRun(script []byte, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (util.Uint256, uint32, error) { + return a.sendWrapper(a.MakeTunedRun(script, attrs, txHook)) +} + +// SendUncheckedRun creates a transaction with the given executable script and +// attributes that can use up to sysfee GAS for its execution, allowing to modify +// this transaction before it's signed (see also MakeUncheckedRun). This +// transaction is then sent to the network. +func (a *Actor) SendUncheckedRun(script []byte, sysfee int64, attrs []transaction.Attribute, txHook TransactionModifier) (util.Uint256, uint32, error) { + return a.sendWrapper(a.MakeUncheckedRun(script, sysfee, attrs, txHook)) +} diff --git a/pkg/rpcclient/actor/actor_test.go b/pkg/rpcclient/actor/actor_test.go new file mode 100644 index 000000000..46cdd3906 --- /dev/null +++ b/pkg/rpcclient/actor/actor_test.go @@ -0,0 +1,254 @@ +package actor + +import ( + "errors" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "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/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" +) + +type RPCClient struct { + err error + invRes *result.Invoke + netFee int64 + bCount uint32 + version *result.Version + hash util.Uint256 +} + +func (r *RPCClient) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + return r.invRes, r.err +} +func (r *RPCClient) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + return r.invRes, r.err +} +func (r *RPCClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) { + return r.invRes, r.err +} +func (r *RPCClient) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) { + return r.netFee, r.err +} +func (r *RPCClient) GetBlockCount() (uint32, error) { + return r.bCount, r.err +} +func (r *RPCClient) GetVersion() (*result.Version, error) { + verCopy := *r.version + return &verCopy, r.err +} +func (r *RPCClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) { + return r.hash, r.err +} + +func testRPCAndAccount(t *testing.T) (*RPCClient, *wallet.Account) { + client := &RPCClient{ + version: &result.Version{ + Protocol: result.Protocol{ + Network: netmode.UnitTestNet, + MillisecondsPerBlock: 1000, + ValidatorsCount: 7, + }, + }, + } + acc, err := wallet.NewAccount() + require.NoError(t, err) + return client, acc +} + +func TestNew(t *testing.T) { + client, acc := testRPCAndAccount(t) + + // No signers. + _, err := New(client, nil) + require.Error(t, err) + + _, err = New(client, []SignerAccount{}) + require.Error(t, err) + + // Good simple. + a, err := NewSimple(client, acc) + require.NoError(t, err) + require.Equal(t, 1, len(a.signers)) + require.Equal(t, 1, len(a.txSigners)) + require.Equal(t, transaction.CalledByEntry, a.signers[0].Signer.Scopes) + require.Equal(t, transaction.CalledByEntry, a.txSigners[0].Scopes) + + // Contractless account. + badAcc, err := wallet.NewAccount() + require.NoError(t, err) + badAccHash := badAcc.Contract.ScriptHash() + badAcc.Contract = nil + + signers := []SignerAccount{{ + Signer: transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.None, + }, + Account: acc, + }, { + Signer: transaction.Signer{ + Account: badAccHash, + Scopes: transaction.CalledByEntry, + }, + Account: badAcc, + }} + + _, err = New(client, signers) + require.Error(t, err) + + // GetVersion returning error. + client.err = errors.New("bad") + _, err = NewSimple(client, acc) + require.Error(t, err) + client.err = nil + + // Account mismatch. + acc2, err := wallet.NewAccount() + require.NoError(t, err) + signers = []SignerAccount{{ + Signer: transaction.Signer{ + Account: acc2.Contract.ScriptHash(), + Scopes: transaction.None, + }, + Account: acc, + }, { + Signer: transaction.Signer{ + Account: acc2.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: acc2, + }} + _, err = New(client, signers) + require.Error(t, err) + + // Good multiaccount. + signers[0].Signer.Account = acc.Contract.ScriptHash() + a, err = New(client, signers) + require.NoError(t, err) + require.Equal(t, 2, len(a.signers)) + require.Equal(t, 2, len(a.txSigners)) +} + +func TestSimpleWrappers(t *testing.T) { + client, acc := testRPCAndAccount(t) + origVer := *client.version + + a, err := NewSimple(client, acc) + require.NoError(t, err) + + client.netFee = 42 + nf, err := a.CalculateNetworkFee(new(transaction.Transaction)) + require.NoError(t, err) + require.Equal(t, int64(42), nf) + + client.bCount = 100500 + bc, err := a.GetBlockCount() + require.NoError(t, err) + require.Equal(t, uint32(100500), bc) + + require.Equal(t, netmode.UnitTestNet, a.GetNetwork()) + client.version.Protocol.Network = netmode.TestNet + require.Equal(t, netmode.UnitTestNet, a.GetNetwork()) + require.Equal(t, origVer, a.GetVersion()) + + a, err = NewSimple(client, acc) + require.NoError(t, err) + require.Equal(t, netmode.TestNet, a.GetNetwork()) + require.Equal(t, *client.version, a.GetVersion()) + + client.hash = util.Uint256{1, 2, 3} + h, vub, err := a.Send(&transaction.Transaction{ValidUntilBlock: 123}) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(123), vub) +} + +func TestSign(t *testing.T) { + client, acc := testRPCAndAccount(t) + acc2, err := wallet.NewAccount() + require.NoError(t, err) + + a, err := New(client, []SignerAccount{{ + Signer: transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.None, + }, + Account: acc, + }, { + Signer: transaction.Signer{ + Account: acc2.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: &wallet.Account{ // Looks like acc2, but has no private key. + Address: acc2.Address, + EncryptedWIF: acc2.EncryptedWIF, + Contract: acc2.Contract, + }, + }}) + require.NoError(t, err) + + script := []byte{1, 2, 3} + client.hash = util.Uint256{2, 5, 6} + client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script} + + tx, err := a.MakeUnsignedRun(script, nil) + require.NoError(t, err) + require.Error(t, a.Sign(tx)) + _, _, err = a.SignAndSend(tx) + require.Error(t, err) +} + +func TestSenders(t *testing.T) { + client, acc := testRPCAndAccount(t) + a, err := NewSimple(client, acc) + require.NoError(t, err) + script := []byte{1, 2, 3} + + // Bad. + client.invRes = &result.Invoke{State: "FAULT", GasConsumed: 3, Script: script} + _, _, err = a.SendCall(util.Uint160{1}, "method", 42) + require.Error(t, err) + _, _, err = a.SendTunedCall(util.Uint160{1}, "method", nil, nil, 42) + require.Error(t, err) + _, _, err = a.SendRun(script) + require.Error(t, err) + _, _, err = a.SendTunedRun(script, nil, nil) + require.Error(t, err) + _, _, err = a.SendUncheckedRun(script, 1, nil, func(t *transaction.Transaction) error { + return errors.New("bad") + }) + require.Error(t, err) + + // Good. + client.hash = util.Uint256{2, 5, 6} + client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script} + h, vub, err := a.SendCall(util.Uint160{1}, "method", 42) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(8), vub) + + h, vub, err = a.SendTunedCall(util.Uint160{1}, "method", nil, nil, 42) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(8), vub) + + h, vub, err = a.SendRun(script) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(8), vub) + + h, vub, err = a.SendTunedRun(script, nil, nil) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(8), vub) + + h, vub, err = a.SendUncheckedRun(script, 1, nil, nil) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(8), vub) +} diff --git a/pkg/rpcclient/actor/maker.go b/pkg/rpcclient/actor/maker.go new file mode 100644 index 000000000..57ce83adb --- /dev/null +++ b/pkg/rpcclient/actor/maker.go @@ -0,0 +1,195 @@ +package actor + +import ( + "errors" + "fmt" + + "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/vmstate" +) + +// TransactionCheckerModifier is a callback that receives the result of +// test-invocation and the transaction that can perform the same invocation +// on chain. This callback is accepted by methods that create transactions, it +// can examine both arguments and return an error if there is anything wrong +// there which will abort the creation process. Notice that when used this +// callback is completely responsible for invocation result checking, including +// checking for HALT execution state (so if you don't check for it in a callback +// you can send a transaction that is known to end up in FAULT state). It can +// also modify the transaction (see TransactionModifier). +type TransactionCheckerModifier func(r *result.Invoke, t *transaction.Transaction) error + +// TransactionModifier is a callback that receives the transaction before +// it's signed from a method that creates signed transactions. It can check +// fees and other fields of the transaction and return an error if there is +// anything wrong there which will abort the creation process. It also can modify +// Nonce, SystemFee, NetworkFee and ValidUntilBlock values taking full +// responsibility on the effects of these modifications (smaller fee values, too +// low or too high ValidUntilBlock or bad Nonce can render transaction invalid). +// Modifying other fields is not supported. Mostly it's useful for increasing +// fee values since by default they're just enough for transaction to be +// successfully accepted and executed. +type TransactionModifier func(t *transaction.Transaction) error + +// MakeCall creates a transaction that calls the given method of the given +// contract with the given parameters. Test call is performed and checked for +// HALT status, if more checks are needed or transaction should have some +// additional attributes use MakeTunedCall. +func (a *Actor) MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error) { + return a.MakeTunedCall(contract, method, nil, nil, params...) +} + +// MakeTunedCall creates a transaction with the given attributes that calls the +// given method of the given contract with the given parameters. It's filtered +// through the provided callback (see TransactionCheckerModifier documentation), +// so the process can be aborted and transaction can be modified before signing. +// If no callback is given then the result is checked for HALT state. +func (a *Actor) MakeTunedCall(contract util.Uint160, method string, attrs []transaction.Attribute, txHook TransactionCheckerModifier, params ...interface{}) (*transaction.Transaction, error) { + r, err := a.Call(contract, method, params...) + return a.makeUncheckedWrapper(r, err, attrs, txHook) +} + +// MakeRun creates a transaction with the given executable script. Test +// invocation of this script is performed and expected to end up in HALT +// state. If more checks are needed or transaction should have some additional +// attributes use MakeTunedRun. +func (a *Actor) MakeRun(script []byte) (*transaction.Transaction, error) { + return a.MakeTunedRun(script, nil, nil) +} + +// MakeTunedRun creates a transaction with the given attributes that executes +// the given script. It's filtered through the provided callback (see +// TransactionCheckerModifier documentation), so the process can be aborted and +// transaction can be modified before signing. If no callback is given then the +// result is checked for HALT state. +func (a *Actor) MakeTunedRun(script []byte, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (*transaction.Transaction, error) { + r, err := a.Run(script) + return a.makeUncheckedWrapper(r, err, attrs, txHook) +} + +func (a *Actor) makeUncheckedWrapper(r *result.Invoke, err error, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (*transaction.Transaction, error) { + if err != nil { + return nil, fmt.Errorf("test invocation failed: %w", err) + } + return a.MakeUncheckedRun(r.Script, r.GasConsumed, attrs, func(tx *transaction.Transaction) error { + if txHook == nil { + if r.State != vmstate.Halt.String() { + return fmt.Errorf("script failed (%s state) due to an error: %s", r.State, r.FaultException) + } + return nil + } + return txHook(r, tx) + }) +} + +// MakeUncheckedRun creates a transaction with the given attributes that executes +// the given script and is expected to use up to sysfee GAS for its execution. +// The transaction is filtered through the provided callback (see +// TransactionModifier documentation), so the process can be aborted and +// transaction can be modified before signing. This method is mostly useful when +// test invocation is already performed and the script and required system fee +// values are already known. +func (a *Actor) MakeUncheckedRun(script []byte, sysfee int64, attrs []transaction.Attribute, txHook TransactionModifier) (*transaction.Transaction, error) { + tx, err := a.MakeUnsignedUncheckedRun(script, sysfee, attrs) + if err != nil { + return nil, err + } + if txHook != nil { + err = txHook(tx) + if err != nil { + return nil, err + } + } + err = a.Sign(tx) + if err != nil { + return nil, err + } + return tx, nil +} + +// MakeUnsignedCall creates an unsigned transaction with the given attributes +// that calls the given method of the given contract with the given parameters. +// Test-invocation is performed and is expected to end up in HALT state, the +// transaction returned has correct SystemFee and NetworkFee values. +func (a *Actor) MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error) { + r, err := a.Call(contract, method, params...) + return a.makeUnsignedWrapper(r, err, attrs) +} + +// MakeUnsignedRun creates an unsigned transaction with the given attributes +// that executes the given script. Test-invocation is performed and is expected +// to end up in HALT state, the transaction returned has correct SystemFee and +// NetworkFee values. +func (a *Actor) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) { + r, err := a.Run(script) + return a.makeUnsignedWrapper(r, err, attrs) +} + +func (a *Actor) makeUnsignedWrapper(r *result.Invoke, err error, attrs []transaction.Attribute) (*transaction.Transaction, error) { + if err != nil { + return nil, fmt.Errorf("failed to test-invoke: %w", err) + } + if r.State != vmstate.Halt.String() { + return nil, fmt.Errorf("test invocation faulted (%s): %s", r.State, r.FaultException) + } + return a.MakeUnsignedUncheckedRun(r.Script, r.GasConsumed, attrs) +} + +// MakeUnsignedUncheckedRun creates an unsigned transaction containing the given +// script with the system fee value and attributes. It's expected to be used when +// test invocation is already done and the script and system fee value are already +// known to be good, so it doesn't do test invocation internally. But it fills +// Signers with Actor's signers, calculates proper ValidUntilBlock and NetworkFee +// values. The resulting transaction can be changed in its Nonce, SystemFee, +// NetworkFee and ValidUntilBlock values and then be signed and sent or +// exchanged via context.ParameterContext. +func (a *Actor) MakeUnsignedUncheckedRun(script []byte, sysFee int64, attrs []transaction.Attribute) (*transaction.Transaction, error) { + var err error + + if len(script) == 0 { + return nil, errors.New("empty script") + } + if sysFee < 0 { + return nil, errors.New("negative system fee") + } + + tx := transaction.New(script, sysFee) + tx.Signers = a.txSigners + tx.Attributes = attrs + + tx.ValidUntilBlock, err = a.CalculateValidUntilBlock() + if err != nil { + return nil, fmt.Errorf("calculating validUntilBlock: %w", err) + } + + tx.Scripts = make([]transaction.Witness, len(a.signers)) + for i := range a.signers { + if !a.signers[i].Account.Contract.Deployed { + tx.Scripts[i].VerificationScript = a.signers[i].Account.Contract.Script + } + } + // CalculateNetworkFee doesn't call Hash or Size, only serializes the + // transaction via Bytes, so it's safe wrt internal caching. + tx.NetworkFee, err = a.client.CalculateNetworkFee(tx) + if err != nil { + return nil, fmt.Errorf("calculating network fee: %w", err) + } + + return tx, nil +} + +// CalculateValidUntilBlock returns correct ValidUntilBlock value for a new +// transaction relative to the current blockchain height. It uses "height + +// number of validators + 1" formula suggesting shorter transaction lifetime +// than the usual "height + MaxValidUntilBlockIncrement" approach. Shorter +// lifetime can be useful to control transaction acceptance wait time because +// it can't be added into a block after ValidUntilBlock. +func (a *Actor) CalculateValidUntilBlock() (uint32, error) { + blockCount, err := a.client.GetBlockCount() + if err != nil { + return 0, fmt.Errorf("can't get block count: %w", err) + } + return blockCount + uint32(a.version.Protocol.ValidatorsCount) + 1, nil +} diff --git a/pkg/rpcclient/actor/maker_test.go b/pkg/rpcclient/actor/maker_test.go new file mode 100644 index 000000000..0e741b7cd --- /dev/null +++ b/pkg/rpcclient/actor/maker_test.go @@ -0,0 +1,161 @@ +package actor + +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/stretchr/testify/require" +) + +func TestCalculateValidUntilBlock(t *testing.T) { + client, acc := testRPCAndAccount(t) + a, err := NewSimple(client, acc) + require.NoError(t, err) + + client.err = errors.New("error") + _, err = a.CalculateValidUntilBlock() + require.Error(t, err) + + client.err = nil + client.bCount = 42 + vub, err := a.CalculateValidUntilBlock() + require.NoError(t, err) + require.Equal(t, uint32(42+7+1), vub) +} + +func TestMakeUnsigned(t *testing.T) { + client, acc := testRPCAndAccount(t) + a, err := NewSimple(client, acc) + require.NoError(t, err) + + // Bad parameters. + script := []byte{1, 2, 3} + _, err = a.MakeUnsignedUncheckedRun(script, -1, nil) + require.Error(t, err) + _, err = a.MakeUnsignedUncheckedRun([]byte{}, 1, nil) + require.Error(t, err) + _, err = a.MakeUnsignedUncheckedRun(nil, 1, nil) + require.Error(t, err) + + // RPC error. + client.err = errors.New("err") + _, err = a.MakeUnsignedUncheckedRun(script, 1, nil) + require.Error(t, err) + + // Good unchecked. + client.netFee = 42 + client.bCount = 100500 + client.err = nil + tx, err := a.MakeUnsignedUncheckedRun(script, 1, nil) + require.NoError(t, err) + require.Equal(t, script, tx.Script) + require.Equal(t, 1, len(tx.Signers)) + require.Equal(t, acc.Contract.ScriptHash(), tx.Signers[0].Account) + require.Equal(t, 1, len(tx.Scripts)) + require.Equal(t, acc.Contract.Script, tx.Scripts[0].VerificationScript) + require.Nil(t, tx.Scripts[0].InvocationScript) + + // Bad run. + client.err = errors.New("") + _, err = a.MakeUnsignedRun(script, nil) + require.Error(t, err) + + // Faulted run. + client.invRes = &result.Invoke{State: "FAULT", GasConsumed: 3, Script: script} + client.err = nil + _, err = a.MakeUnsignedRun(script, nil) + require.Error(t, err) + + // Good run. + client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script} + _, err = a.MakeUnsignedRun(script, nil) + require.NoError(t, err) +} + +func TestMakeSigned(t *testing.T) { + client, acc := testRPCAndAccount(t) + a, err := NewSimple(client, acc) + require.NoError(t, err) + + // Bad script. + _, err = a.MakeUncheckedRun(nil, 0, nil, nil) + require.Error(t, err) + + // Good, no hook. + script := []byte{1, 2, 3} + _, err = a.MakeUncheckedRun(script, 0, nil, nil) + require.NoError(t, err) + + // Bad, can't sign because of a hook. + _, err = a.MakeUncheckedRun(script, 0, nil, func(t *transaction.Transaction) error { + t.Signers = append(t.Signers, transaction.Signer{}) + return nil + }) + require.Error(t, err) + + // Bad, hook returns an error. + _, err = a.MakeUncheckedRun(script, 0, nil, func(t *transaction.Transaction) error { + return errors.New("") + }) + require.Error(t, err) + + // Good with a hook. + tx, err := a.MakeUncheckedRun(script, 0, nil, func(t *transaction.Transaction) error { + t.ValidUntilBlock = 777 + return nil + }) + require.NoError(t, err) + require.Equal(t, uint32(777), tx.ValidUntilBlock) + + // Checked + + // Bad, invocation fails. + client.err = errors.New("") + _, err = a.MakeTunedRun(script, nil, func(r *result.Invoke, t *transaction.Transaction) error { + return nil + }) + require.Error(t, err) + + // Bad, hook returns an error. + client.err = nil + client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script} + _, err = a.MakeTunedRun(script, nil, func(r *result.Invoke, t *transaction.Transaction) error { + return errors.New("") + }) + require.Error(t, err) + + // Good, no hook. + _, err = a.MakeTunedRun(script, []transaction.Attribute{{Type: transaction.HighPriority}}, nil) + require.NoError(t, err) + _, err = a.MakeRun(script) + require.NoError(t, err) + + // Bad, invocation returns FAULT. + client.invRes = &result.Invoke{State: "FAULT", GasConsumed: 3, Script: script} + _, err = a.MakeTunedRun(script, nil, nil) + require.Error(t, err) + + // Good, invocation returns FAULT, but callback ignores it. + _, err = a.MakeTunedRun(script, nil, func(r *result.Invoke, t *transaction.Transaction) error { + return nil + }) + require.NoError(t, err) + + // Good, via call and with a callback. + _, err = a.MakeTunedCall(util.Uint160{}, "something", []transaction.Attribute{{Type: transaction.HighPriority}}, func(r *result.Invoke, t *transaction.Transaction) error { + return nil + }, "param", 1) + require.NoError(t, err) + + // Bad, it still is a FAULT. + _, err = a.MakeCall(util.Uint160{}, "method") + require.Error(t, err) + + // Good. + client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script} + _, err = a.MakeCall(util.Uint160{}, "method", 1) + require.NoError(t, err) +} diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go index 049f46a46..b278997ba 100644 --- a/pkg/rpcclient/rpc.go +++ b/pkg/rpcclient/rpc.go @@ -1006,6 +1006,9 @@ func (c *Client) ValidateAddress(address string) error { // current blockchain height + number of validators. Number of validators // is the length of blockchain validators list got from GetNextBlockValidators() // method. Validators count is being cached and updated every 100 blocks. +// +// Deprecated: please use (*Actor).CalculateValidUntilBlock. This method will be +// removed in future versions. func (c *Client) CalculateValidUntilBlock() (uint32, error) { var ( result uint32